我认为 v2.0 更好......他们有一些很好的“如何:......”示例,但书签的行为似乎不像 Table 那样明显......书签由两个XML 元素BookmarkStart &书签结束。我们有一些带有文本作为书签的模板,我们只是想用一些其他文本替换书签......没有奇怪的格式正在发生,但我如何选择/替换书签文本?
11 回答
这是我以你们为灵感后的方法:
IDictionary<String, BookmarkStart> bookmarkMap =
new Dictionary<String, BookmarkStart>();
foreach (BookmarkStart bookmarkStart in file.MainDocumentPart.RootElement.Descendants<BookmarkStart>())
{
bookmarkMap[bookmarkStart.Name] = bookmarkStart;
}
foreach (BookmarkStart bookmarkStart in bookmarkMap.Values)
{
Run bookmarkText = bookmarkStart.NextSibling<Run>();
if (bookmarkText != null)
{
bookmarkText.GetFirstChild<Text>().Text = "blah";
}
}
用单个内容(可能是多个文本块)替换书签。
public static void InsertIntoBookmark(BookmarkStart bookmarkStart, string text)
{
OpenXmlElement elem = bookmarkStart.NextSibling();
while (elem != null && !(elem is BookmarkEnd))
{
OpenXmlElement nextElem = elem.NextSibling();
elem.Remove();
elem = nextElem;
}
bookmarkStart.Parent.InsertAfter<Run>(new Run(new Text(text)), bookmarkStart);
}
首先,移除 start 和 end 之间的现有内容。然后直接在开始之后(结束之前)添加一个新的运行。
但是,不确定书签在打开时是否在另一个部分或不同的表格单元格中关闭,等等。..
对我来说,现在就足够了。
很多小时后,我写了这个方法:
Public static void ReplaceBookmarkParagraphs(WordprocessingDocument doc, string bookmark, string text)
{
//Find all Paragraph with 'BookmarkStart'
var t = (from el in doc.MainDocumentPart.RootElement.Descendants<BookmarkStart>()
where (el.Name == bookmark) &&
(el.NextSibling<Run>() != null)
select el).First();
//Take ID value
var val = t.Id.Value;
//Find the next sibling 'text'
OpenXmlElement next = t.NextSibling<Run>();
//Set text value
next.GetFirstChild<Text>().Text = text;
//Delete all bookmarkEnd node, until the same ID
deleteElement(next.GetFirstChild<Text>().Parent, next.GetFirstChild<Text>().NextSibling(), val, true);
}
之后,我打电话给:
Public static bool deleteElement(OpenXmlElement parentElement, OpenXmlElement elem, string id, bool seekParent)
{
bool found = false;
//Loop until I find BookmarkEnd or null element
while (!found && elem != null && (!(elem is BookmarkEnd) || (((BookmarkEnd)elem).Id.Value != id)))
{
if (elem.ChildElements != null && elem.ChildElements.Count > 0)
{
found = deleteElement(elem, elem.FirstChild, id, false);
}
if (!found)
{
OpenXmlElement nextElem = elem.NextSibling();
elem.Remove();
elem = nextElem;
}
}
if (!found)
{
if (elem == null)
{
if (!(parentElement is Body) && seekParent)
{
//Try to find bookmarkEnd in Sibling nodes
found = deleteElement(parentElement.Parent, parentElement.NextSibling(), id, true);
}
}
else
{
if (elem is BookmarkEnd && ((BookmarkEnd)elem).Id.Value == id)
{
found = true;
}
}
}
return found;
}
如果您没有空书签,此代码运行良好。我希望它可以帮助某人。
我在 10 分钟前才发现这一点,所以请原谅代码的骇人听闻的性质。
首先,我编写了一个辅助递归辅助函数来查找所有书签:
private static Dictionary<string, BookmarkEnd> FindBookmarks(OpenXmlElement documentPart, Dictionary<string, BookmarkEnd> results = null, Dictionary<string, string> unmatched = null )
{
results = results ?? new Dictionary<string, BookmarkEnd>();
unmatched = unmatched ?? new Dictionary<string,string>();
foreach (var child in documentPart.Elements())
{
if (child is BookmarkStart)
{
var bStart = child as BookmarkStart;
unmatched.Add(bStart.Id, bStart.Name);
}
if (child is BookmarkEnd)
{
var bEnd = child as BookmarkEnd;
foreach (var orphanName in unmatched)
{
if (bEnd.Id == orphanName.Key)
results.Add(orphanName.Value, bEnd);
}
}
FindBookmarks(child, results, unmatched);
}
return results;
}
这会返回给我一个字典,我可以用它来分隔我的替换列表并在书签之后添加文本:
var bookMarks = FindBookmarks(doc.MainDocumentPart.Document);
foreach( var end in bookMarks )
{
var textElement = new Text("asdfasdf");
var runElement = new Run(textElement);
end.Value.InsertAfterSelf(runElement);
}
据我所知,插入和替换书签看起来更难。当我使用 InsertAt 而不是 InsertIntoSelf 时,我得到:“非复合元素没有子元素。” YMMV
我从答案中获取了代码,并在特殊情况下遇到了几个问题:
- 您可能想忽略隐藏的书签。如果名称以 _(下划线)开头,则隐藏书签
- 如果书签用于更多的 TableCell,您将在 BookmarkStart 中找到它在行的第一个单元格中,其属性 ColumnFirst 指的是书签开始的单元格的从 0 开始的列索引。ColumnLast 指的是书签结束的单元格,对于我的特殊情况,它始终是 ColumnFirst == ColumnLast (书签仅标记一列)。在这种情况下,您也不会找到 BookmarkEnd。
- 书签可以是空的,所以 BookmarkStart 直接跟在 BookmarkEnd 之后,在这种情况下你可以调用
bookmarkStart.Parent.InsertAfter(new Run(new Text("Hello World")), bookmarkStart)
- 此外,书签可以包含许多文本元素,因此您可能希望删除所有其他元素,否则书签的部分可能会被替换,而其他后续部分将保留。
- 而且我不确定我的最后一次 hack 是否有必要,因为我不知道 OpenXML 的所有限制,但是在发现前 4 个之后,我也不再相信 Run 会有一个兄弟姐妹,有一个文本的孩子。因此,我只查看我所有的兄弟姐妹(直到 BookmarEnd 与 BookmarkStart 具有相同的 ID)并检查所有孩子,直到找到任何文本。- 如果有必要,也许有更多 OpenXML 经验的人可以回答?
你可以在这里查看我的具体实现)
希望这对遇到相同问题的一些人有所帮助。
这里的大多数解决方案都假设在运行之前开始和在运行之后结束的常规书签模式,这并不总是正确的,例如,如果书签从一个段落或表格开始并在另一个段落的某个地方结束(就像其他人指出的那样)。如何使用文档顺序来处理书签未放置在常规结构中的情况 - 文档顺序仍然会找到所有相关的文本节点,然后可以替换这些节点。只需执行 root.DescendantNodes().Where(xtext or bookmarkstart or bookmark end) ,它将按文档顺序遍历,然后可以替换在看到书签开始节点之后但在看到结束节点之前出现的文本节点。
这是我如何做到这一点和VB在bookmarkStart和BookmarkEnd之间添加/替换文本。
<w:bookmarkStart w:name="forbund_kort" w:id="0" />
- <w:r>
<w:t>forbund_kort</w:t>
</w:r>
<w:bookmarkEnd w:id="0" />
Imports DocumentFormat.OpenXml.Packaging
Imports DocumentFormat.OpenXml.Wordprocessing
Public Class PPWordDocx
Public Sub ChangeBookmarks(ByVal path As String)
Try
Dim doc As WordprocessingDocument = WordprocessingDocument.Open(path, True)
'Read the entire document contents using the GetStream method:
Dim bookmarkMap As IDictionary(Of String, BookmarkStart) = New Dictionary(Of String, BookmarkStart)()
Dim bs As BookmarkStart
For Each bs In doc.MainDocumentPart.RootElement.Descendants(Of BookmarkStart)()
bookmarkMap(bs.Name) = bs
Next
For Each bs In bookmarkMap.Values
Dim bsText As DocumentFormat.OpenXml.OpenXmlElement = bs.NextSibling
If Not bsText Is Nothing Then
If TypeOf bsText Is BookmarkEnd Then
'Add Text element after start bookmark
bs.Parent.InsertAfter(New Run(New Text(bs.Name)), bs)
Else
'Change Bookmark Text
If TypeOf bsText Is Run Then
If bsText.GetFirstChild(Of Text)() Is Nothing Then
bsText.InsertAt(New Text(bs.Name), 0)
End If
bsText.GetFirstChild(Of Text)().Text = bs.Name
End If
End If
End If
Next
doc.MainDocumentPart.RootElement.Save()
doc.Close()
Catch ex As Exception
Throw ex
End Try
End Sub
End Class
我需要用表格替换书签的文本(书签名称为“表格”)。这是我的方法:
public void ReplaceBookmark( DatasetToTable( ds ) )
{
MainDocumentPart mainPart = myDoc.MainDocumentPart;
Body body = mainPart.Document.GetFirstChild<Body>();
var bookmark = body.Descendants<BookmarkStart>()
.Where( o => o.Name == "Table" )
.FirstOrDefault();
var parent = bookmark.Parent; //bookmark's parent element
if (ds!=null)
{
parent.InsertAfterSelf( DatasetToTable( ds ) );
parent.Remove();
}
mainPart.Document.Save();
}
public Table DatasetToTable( DataSet ds )
{
Table table = new Table();
//creating table;
return table;
}
希望这可以帮助
这是我在 VB.NET 中的操作方式:
For Each curBookMark In contractBookMarkStarts
''# Get the "Run" immediately following the bookmark and then
''# get the Run's "Text" field
runAfterBookmark = curBookMark.NextSibling(Of Wordprocessing.Run)()
textInRun = runAfterBookmark.LastChild
''# Decode the bookmark to a contract attribute
lines = DecodeContractDataToContractDocFields(curBookMark.Name, curContract).Split(vbCrLf)
''# If there are multiple lines returned then some work needs to be done to create
''# the necessary Run/Text fields to hold lines 2 thru n. If just one line then set the
''# Text field to the attribute from the contract
For ptr = 0 To lines.Count - 1
line = lines(ptr)
If ptr = 0 Then
textInRun.Text = line.Trim()
Else
''# Add a <br> run/text component then add next line
newRunForLf = New Run(runAfterBookmark.OuterXml)
newRunForLf.LastChild.Remove()
newBreak = New Break()
newRunForLf.Append(newBreak)
newRunForText = New Run(runAfterBookmark.OuterXml)
DirectCast(newRunForText.LastChild, Text).Text = line.Trim
curBookMark.Parent.Append(newRunForLf)
curBookMark.Parent.Append(newRunForText)
End If
Next
Next
接受的答案和其他一些答案对书签在文档结构中的位置进行了假设。这是我的 C# 代码,它可以处理替换跨越多个段落的书签,并正确替换不在段落边界开始和结束的书签。仍然不完美,但更接近......希望它有用。如果您找到更多改进方法,请编辑!
private static void ReplaceBookmarkParagraphs(MainDocumentPart doc, string bookmark, IEnumerable<OpenXmlElement> paras) {
var start = doc.Document.Descendants<BookmarkStart>().Where(x => x.Name == bookmark).First();
var end = doc.Document.Descendants<BookmarkEnd>().Where(x => x.Id.Value == start.Id.Value).First();
OpenXmlElement current = start;
var done = false;
while ( !done && current != null ) {
OpenXmlElement next;
next = current.NextSibling();
if ( next == null ) {
var parentNext = current.Parent.NextSibling();
while ( !parentNext.HasChildren ) {
var toRemove = parentNext;
parentNext = parentNext.NextSibling();
toRemove.Remove();
}
next = current.Parent.NextSibling().FirstChild;
current.Parent.Remove();
}
if ( next is BookmarkEnd ) {
BookmarkEnd maybeEnd = (BookmarkEnd)next;
if ( maybeEnd.Id.Value == start.Id.Value ) {
done = true;
}
}
if ( current != start ) {
current.Remove();
}
current = next;
}
foreach ( var p in paras ) {
end.Parent.InsertBeforeSelf(p);
}
}
这是我最终得到的结果 - 不是 100% 完美,但适用于简单的书签和要插入的简单文本:
private void FillBookmarksUsingOpenXml(string sourceDoc, string destDoc, Dictionary<string, string> bookmarkData)
{
string wordmlNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
// Make a copy of the template file.
File.Copy(sourceDoc, destDoc, true);
//Open the document as an Open XML package and extract the main document part.
using (WordprocessingDocument wordPackage = WordprocessingDocument.Open(destDoc, true))
{
MainDocumentPart part = wordPackage.MainDocumentPart;
//Setup the namespace manager so you can perform XPath queries
//to search for bookmarks in the part.
NameTable nt = new NameTable();
XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
nsManager.AddNamespace("w", wordmlNamespace);
//Load the part's XML into an XmlDocument instance.
XmlDocument xmlDoc = new XmlDocument(nt);
xmlDoc.Load(part.GetStream());
//Iterate through the bookmarks.
foreach (KeyValuePair<string, string> bookmarkDataVal in bookmarkData)
{
var bookmarks = from bm in part.Document.Body.Descendants<BookmarkStart>()
select bm;
foreach (var bookmark in bookmarks)
{
if (bookmark.Name == bookmarkDataVal.Key)
{
Run bookmarkText = bookmark.NextSibling<Run>();
if (bookmarkText != null) // if the bookmark has text replace it
{
bookmarkText.GetFirstChild<Text>().Text = bookmarkDataVal.Value;
}
else // otherwise append new text immediately after it
{
var parent = bookmark.Parent; // bookmark's parent element
Text text = new Text(bookmarkDataVal.Value);
Run run = new Run(new RunProperties());
run.Append(text);
// insert after bookmark parent
parent.Append(run);
}
//bk.Remove(); // we don't want the bookmark anymore
}
}
}
//Write the changes back to the document part.
xmlDoc.Save(wordPackage.MainDocumentPart.GetStream(FileMode.Create));
}
}