用C#來解析PDF文件
1. 介紹
這個項目讓你可以去讀取并解析一個PDF文件,并將其內(nèi)部結(jié)構(gòu)展示出來. PDF文件的格式標準文檔可以從Adobe那兒獲取到. 這個項目基于“PDF指南,第六版,Adobe便攜文檔格式1.7 2006年11月”. 它是一個恐怕有1310頁的大部頭. 本文提供了對這份文檔的簡潔概述. 與此相關(guān)的項目定義了用來讀取和解析PDF文件的C#類. 為了測試這些類,附帶的測試程序PdfFileAnalyzer讓你可以去讀取一個PDF文件,分析它并展示和保存結(jié)果. 程序?qū)DF文件分割成單獨每頁的描述,字體,圖片和其它對象. 有兩種類型的PDF文件不受此程序的支持: 加密文件和多代文件.
這個程序的1.1版本允許世界各地使用點符號作為小數(shù)分隔符的程序員來編譯和運行程序.
1.2版本則修復了一個有關(guān)使用跨多個引用流來讀取PDF文檔的問題. 1.2之前的版本對此場景只會以一個對象數(shù)字重復的錯誤而終止運行.
2. 概要
PDF格式的文件,借助Adobe Acrobat軟件,可以在各種屏幕上顯示查看,使用各種打印機打印。但是,如果使用二進制文件編輯器打開PDF文件,你會發(fā)現(xiàn)文件的大部分是不可讀的,有小部分是可讀的,如下:
1 0 obj <</Lang(en-CA)/MarkInfo<</Marked true>>/Pages 2 0 R /StructTreeRoot 10 0 R/Type/Catalog>> endobj 2 0 obj <</Count 1/Kids[4 0 R]/Type/Pages>> endobj 4 0 obj <</Contents 5 0 R/Group <</CS/DeviceRGB /S/Transparency /Type/Group>> /MediaBox[0 0 612 792] /Parent 2 0 R /Resources <</Font <</F1 6 0 R /F2 8 0 R>> /ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>> /StructParents 0/Tabs/S/Type/Page>> endobj 5 0 obj <</Filter/FlateDecode/Length 2319>> stream . . . endstream endobj
看上去,該文件是由嵌套在“n 0 OBJ ”和“ endobj ”關(guān)鍵詞之間的對象組成的,術(shù)語PDF也就是間接對象的意思。 “obj”前面的數(shù)字是對象編號和第幾代對象標識, 雙尖括號中的內(nèi)容表示數(shù)據(jù)字典對象,中括號中的內(nèi)容表示數(shù)組對象, 以斜杠/ 開始的內(nèi)容表示參數(shù)名稱 (例如: /Pages)。上例中的第一項 “1 0 obj” 表示文檔的目錄或者文檔的根對象。文檔目錄的字典對象 “/Pages 2 0 R”,指向定義頁碼樹對象的引用。按照這樣推算,編號為2的對象包含指向 “/Kids[4 0 R]”的頁面的引用,是一個頁面文檔。 編號為4的對象是唯一的一個頁面定義, 頁面大小為612*792點, 換句話說,也就是8.5” * 11” (1” 代表72 點)點。該頁面使用了兩種字體F1和F2,這兩種字體分別在編號為6和8的對象中定義。該頁面的內(nèi)容在編號為5的對象中描述,該對象中包含頁面繪圖的流信息,示例中的 “. . .”代表這部分流信息。如果使用二進制文件編輯器打開PDF文件,會發(fā)現(xiàn)這部分流信息看起來是一長串不可讀的隨機數(shù),原因是那是壓縮數(shù)據(jù)。流數(shù)據(jù)采用Zlib方法壓縮,壓縮方式由字典對象“/Filter /FlateDecode”描述,被壓縮流的大小為2319字節(jié)。解壓這部分流信息,前面幾行內(nèi)容如下所示:
q 37.08 56.424 537.84 679.18 re W* n /P <</MCID 0>> BDC 0.753 g 36.6 465.43 537.96 24.84 re f* EMC /P <</MCID 1/Lang (x-none)>> BDC BT /F1 18 Tf 1 0 0 1 39.6 718.8 Tm 0 g 0 G [(GRA)29(NOTECH LI)-3(MIT)-4(ED)] TJ ET
這是頁面描述語言的一個小例子。 示例中, “re” 代表矩形,“re” 前面的4個數(shù)字代表矩形的位置和大小,依次為:起點橫坐標、起點縱坐標、寬度、高度。
這個簡單的例子演示了PDF文件內(nèi)部實現(xiàn)的總體思路。從頁面層次結(jié)構(gòu)的根對象開始, 每一頁都定義了諸如字體、圖片、內(nèi)容流的資源,內(nèi)容流由操作符和繪制頁面所需要的參數(shù)構(gòu)成。PDF文件分析器會產(chǎn)生一個對象匯總文件,該文件包含非流對象的其他所有對象。每個數(shù)據(jù)流會被解碼并保存為一個單獨的文件, 頁面描述流保存為文本格式的文件, 圖片流保存為.jpg或.bmp格式的文件,字體流保存為.ttf格式的文件,其他二進制流保存為.bin 格式的文件,文本流保存為.txt格式的文件。通過另一個解析過程,晦澀難懂的頁面描述會被轉(zhuǎn)換為偽C#代碼,如上例中的頁面描述被轉(zhuǎn)為:
SaveGraphicsState(); // q Rectangle(37.08, 56.424, 537.84, 679.18); // re ClippingPathEvenOddRule(); // W* NoPaint(); // n BeginMarkedContentPropList("/P", "<</MCID 0>>"); // BDC GrayLevelForNonStroking(0.753); // g Rectangle(36.6, 465.43, 537.96, 24.84); // re FillEvenOddRule(); // f* EndMarkedContent(); // EMC BeginMarkedContentPropList("/P", "<</Lang(x-none)/MCID 1>>"); // BDC BeginText(); // BT SelectFontAndSize("/F1", 18); // Tf TextMatrix(1, 0, 0, 1, 39.6, 718.8); // Tm GrayLevelForNonStroking(0); // g GrayLevelForStroking(0); // G ShowTextWithGlyphPos("[(GRA)29(NOTECH LI)-3(MIT)-4(ED)]"); // TJ EndTextObject(); // ET
文章接下來的部分將對PDF文件的結(jié)構(gòu)和解析過程進行更為詳細的描述,接下來的章節(jié)包括:對象定義,文件結(jié)構(gòu),文件解析,文件讀取,以及使用PDF文件分析器編程。
3. 免責聲明
pdf 文件分析器能處理大量的文件,這是我在自己的系統(tǒng)上掃描眾多PDF文件的經(jīng)驗。不過,該程序不支持加密文件或者多個代文件(在對象不為零之前的第二個數(shù)字)。在PDF規(guī)格文件之中可用功能的數(shù)量是非常顯著的。這并不可能為一個單的個開發(fā)者系統(tǒng)地測試所有的功能。如果在整個文件分析期間該程序拋出一個異常,將顯示一條錯誤信息,該信息顯示源代碼模塊名和行號。
4.對象定義
PDF文件生成多個對象。在PDF文件分析器項目中每個PDF對象都有一個對應(yīng)的類。所有這些對象類都派生于PDFbase類。對象類定義源代碼是BasicObjects.cs.確卻地PDF對象定義在Adobe pdf文件 規(guī)格第三章之中是有用的
4.1. 基礎(chǔ)的對象
Boolean對象是靠PdfBoolean類來實現(xiàn)的. Boolean在PDF上的定義同C#上的是相同的.
Integer 對象是靠PdfInt類來實現(xiàn)的. PDF上的定義同C#上Int32的定義是相同的.
實數(shù)對象是靠PdfReal類來實現(xiàn)的. PDF上的定義同C#上的Single定義相同.
String 對象是靠PdfStr類來實現(xiàn)的. PDF上的定義同C#相比有所不同. String 是用字節(jié)構(gòu)造出來的,而不是字符. 它被包在圓括號()里面. PdfFileAnalyzer會把包含在圓括號中的C#字符串保存成PDF的字符串. PDF的字符串對于ASCII編碼非常有用.
十六進制字符串獨享是靠PdfHex類來實現(xiàn)的. 它是由每字節(jié)兩個十六進制數(shù)定義,并包在尖括號里面的字符串. PdfFileAnalyzer 將包含在尖括號中的C#字符串保存成PDF十六進制字符串. 對于 PDF 讀取器,字符串和十六進制字符串對象可用于同種目的. 字符串 (AB) 等同于<4142>. PDF 十六進制字符串對于任意編碼的場景非常有用.
Name 對象是靠PdfName類來實現(xiàn)的. Name 對象是由打頭的正斜杠后面跟著一些字符組成的. 例如 /Width. Named 對象用作參數(shù)名稱. PdfFileAnalyzer 將正斜杠開頭的C#字符串保存成Name對象.
Null 對象是靠PdfNull類來實現(xiàn)的. PDF 對于null的定義基本上同C#中的是一樣的.
4.2. 復合的對象
Array 對象是靠 PdfArray 類來實現(xiàn)的. PDF 數(shù)組是一個封裝在一堆中括號中的對象的集合. 一個數(shù)組的對象可以是除了流之外的任何對象.PdfFileAnalyzer 將一個C#數(shù)組中的對象保存成PdfBase類
. 因為所有的對象都繼承自PdfBase,所有在這個數(shù)組中保存多種類型的對象沒有啥問題. 當數(shù)組對象被轉(zhuǎn)換成一個字符串時(使用ToString()方法), 程序會在首位添加中括號. 數(shù)組可以是空的. 下面是一個有六個對象的數(shù)組示例: [120 9.56 true null (string) <414243>].
Dictionary 對象是靠PdfDict類實現(xiàn)的. PDF 字典是一組被包入一對雙尖括號中的鍵值對集合. Dictionary 的鍵是一個對象的名稱,而值則可以是除了流之外的任何對象. PdfFileAnalyzer 將一個鍵值對保存到PdfPair類中. 鍵是一個C#字符串,而值則是一個PdfBase.PdfDict 類有一個PdfPair類的數(shù)組. Dictionary 可以用鍵來訪問. 因而鍵值對的順序沒有啥意義. PdfFileAnalyzer 用鍵來對鍵值對進行排序. 下面是一個有三個鍵值對的字典: <</CropBox [0 0 612 792] /Rotate 0 /Type /Page>>.
Stream 對象是靠PdfStream來實現(xiàn)的. Streams 被用來處理面熟語言,圖形和字體. PDF Stream 由一個字典和一個字節(jié)流組成. 字典中定義了流的參數(shù). 比如流對象中字典的一個鍵值對 /Filter. PDF 文檔定義了10種類型的過濾器. PdfFileAnalyzer 支持了4種. 這是我發(fā)現(xiàn)在實際場景中只會被用到那4種. 壓縮過濾器 FlateDecode 是現(xiàn)在的PDF寫入器最長被用到的過濾器. FlateDecode支持ZLib解壓縮. LZWDecode 壓縮過濾器在過去些年用的比較多. 為了能讀取比較老的PDF文件, 我們的程序支持這個過濾器. ASCII85Decode 過濾器將可被打印的ASCII轉(zhuǎn)換成二進制位. DCTDecode 用于JPEG圖像的壓縮.PdfFileAnalyzer 為前三種實現(xiàn)了解壓縮. DCTDecode 流則以文件擴展名.jpg保存. 它是一個可以被展示的圖片文件.
Object 流在PDF 1.5中被引入. 它是一個包含多個間接對象(在下面會描述道)的流. 上面描述的Stream 對象一次只壓縮一個流. Object 流會將所有包含進來的流壓縮到一個壓縮域中.
多引用流在PDF 1.5中被引入. 它是一個包含多引用表格的流,下文會描述到.
內(nèi)聯(lián)圖片對象是靠 PdfInlineImage來實現(xiàn)的. 它是一個帶有一個流的流. 內(nèi)聯(lián)圖片是頁面描述語言的一部分. 它由BI-開頭圖形, ID-圖形數(shù)據(jù)和EI-結(jié)尾圖形這三個操作符組成. BI 和 ID 之間的區(qū)域是一個圖形字典,而ID 和 EI 之間的區(qū)域則包含圖形數(shù)據(jù).
4.3. 間接對象
間接對象是靠 PdfIndirectObject實現(xiàn)的. 它是一個PDF文檔的主要構(gòu)造塊. 間接對象是任何被包在 “n 0 obj” 和 “endobj”之間的對象. 其它對象可以通過設(shè)定“n 0 R”來引用間接對象. “n”代表對象編號. “0”代表生成編號. 這個程序不支持0之外的生成編號. PDF 規(guī)范允許其它的編號. 多代生成的理念允許PDF的修改操作是在保留原有文件的基礎(chǔ)上追加變更.
對象引用時一種引用間接對象的方法. 例如 /Pages 2 0 R 是目錄對象中的字典里的一項. 它是一個指向 /Pages 對象的指針. pages對象是編號為2的間接對象.
4.4. 操作符和關(guān)鍵詞
操作符和關(guān)鍵詞不被認為是PDF對象. 而PdfFileAnalyzer 程序有一個PdfOp 和一個PdfKeyword 類可以從中得到 PdfBase 的類. 在轉(zhuǎn)換過程中,轉(zhuǎn)換器為每一個可用的字符序列創(chuàng)建了一個 PdfOp 或者PdfKeyword . Pdf文件規(guī)范的附錄A-操作符總結(jié)中列出了所有的操作符. 列表中有73個操作符. 下面是一些操作符的示例: BT-打頭的文本對象, G-用于做記號的設(shè)置灰度操作, m-移動到, re-矩形和Tc-設(shè)置字符間距. 下面是關(guān)鍵詞的示例: stream, obj, endobj, xref.
5. 文件結(jié)構(gòu)
PDF文件由四個部分構(gòu)成: 頭部Header , 主體body, 多引用cross-reference 和附帶簽名 trailer signature.
- Header: 頭部是文件的簽名. 它必須是 %PDF-1.x , x 從 0 到 7.
- Body: 主體區(qū)域包含所有的間接對象.
- Cross-reference: 多引用是一個指向所有間接對象的文件位置指針列表. 有兩種類型的多引用表格. 原始的類型有ASCII字符組成. 新式的是一個包含一個間接對象的流. 信息以二進制數(shù)字編碼. 在多引用表格的結(jié)束部分有一個附件字典. 一個文件可以有超過一個的多引用區(qū)域.
- Trailer signature: 附帶簽名由關(guān)鍵詞“startxref”, 最后一個多引用表格的偏移位, 和結(jié)束簽名 %%EOF 組成. 請注意: 附帶簽名是多引用區(qū)域的一部分.
6. 文件轉(zhuǎn)換
PDF 文件是一個字節(jié)的序列. 一些字節(jié)有特殊的意義.
空格被定義成: null, tab, 換行, 換頁, 回車和間隔.
分隔符被定義成: (, ), <, >, [, ], {, }, /, %, 以及空格字符.
文件轉(zhuǎn)換是由PdfParser 類來完成的. 開始進行轉(zhuǎn)換過程是,程序會設(shè)置文件需要被轉(zhuǎn)換區(qū)域的位置. ParseNextItem() 是提取下一個對象的方法.
解析器跳過空格符和注釋。如果下一個字節(jié)是“(”,判斷對象為一個字符串。如果下一個字節(jié)是“[”,判斷對象是一個數(shù)組。如果接下來的兩個字節(jié)是“<<”,判斷對象是一個字典。如果下一個字節(jié)是“<”,判斷對象是一個十六進制字符串。如果下一個字節(jié)是“/”,判斷對象是一個名稱。如果下一個字節(jié)不是上述任何一種,解析器會采集隨后的字節(jié)直到發(fā)現(xiàn)定界符。定界符不是當前標記符的一部分。標記符可以是整數(shù),實數(shù),操作符或關(guān)鍵詞。在整數(shù)的情況下,程序?qū)⑦M一步搜索對象引用“n 0 R”或間接對象“n 0 obj”中 n 為該整數(shù)的對象。從 ParseNextItem() 返回的值是第4節(jié)“對象的定義”中所述的適當對象。對象的類作為 PdfBase 類返回。
在數(shù)組或字典的情況下,程序?qū)?zhí)行遞歸調(diào)用 ParseNextItem() 來解析數(shù)組或字典的內(nèi)部對象。
7. 文件讀取
PdfDocument 類是 PDF 文件分析的主要類。入口方法是 ReadPdfFile(String FileName)。程序以二進制讀取的方式打開 PDF 文件(一次一個字節(jié))。
文件分析開始于檢查頭部簽名 %PDF-1.x(x為0到7)和結(jié)尾簽名%%EOF。有人會認為,所有的 PDF 生成器會把頭部簽名放在文件的零位置,結(jié)尾簽名放在文件的最后。不幸的是,實際并非如此。程序必須在文件的兩端搜索這兩個簽名。如果頭部簽名不在零位置,所有間接對象的文件位置的指針也必須調(diào)整。
就在結(jié)尾簽名的前面有一個指向最后一個交叉引用表開始位置的指針。
解析器為多引用表設(shè)置文件位置. 如果下一個對象是“xref” 關(guān)鍵詞,我們就有了原來類型的多引用. 否則,它就是新的基于流的多引用. 文件可以有多個多引用表. 文件也可以同時擁有新的和舊的風格的表. 每一個表都有一個對象數(shù)目和指向間接引用開頭的指針的列表. 對于每一個活動對象程序都會創(chuàng)建一個PdfIndirectObject 對象并將其保存在 ObjectArray中. 除了對象的數(shù)字和位置,這個對象的其它東西都是空的. 對于原來的多引用表,其位置是相對于文件而言的. 對于流類型的多引用,位置是相對于一個父間接對象流而言的.
在處理過程中,如果間接對象生成了0之外的數(shù)字, 程序的執(zhí)行就會被終止. PdfFileAnalyzer 不支持多代的形式.
附件字典在交叉引用表的末尾處。分析PDF文件的時候,我們創(chuàng)建了一個帶負對象號的虛擬間接對象用于保存附件字典。
程序在附件字典中尋找四個特定的入口。如果找到/Encrypt入口,表示PDF文件是被加密的,程序的將結(jié)束分析,因為程序不支持分析加密格式的PDF文件。接著程序?qū)ふ?Root目錄對象的對象號。如果找到/XRefStm入口,我們就有了兩種交叉引用的類型。最后如果存在/Prev入口,我們有了另一個用于處理的交叉引用表。
交叉引用的處理完成后,我們擁有所有的間接對象的數(shù)組。 在處理階段,可用信息是對象號和對象位置。下一步,程序遍歷數(shù)組,讀取并解析每一個間接對象,并設(shè)置對象的值。如果對象是流,僅字典部分被解析,因為在這個時候還不知道流的長度。除了上述對象,如果字典和流對象的對象類型和子類型成員是可用的,系統(tǒng)將為字典和流對象設(shè)置這兩個值。
接下來程序遍歷所有的對象,并處理流對象。流對象的對象類型是"/ObjStm"。程序讀取和對象相關(guān)聯(lián)的流,并分解流到多個間接對象上。
接下來程序搜索所有的字典對象和流對象引用的對象字典對象。程序查找鍵值對,例如“/name n 0 R”。加入鍵值對被找到,程序檢查對象類型。如果再對象解析階段沒有設(shè)置對象類型,對象類型將設(shè)置為/name值。
下一步,讀取所有前面沒有讀取的流。系統(tǒng)讀取從文件讀取流。流被解碼并保存到對應(yīng)的文件中。PdfFileAnalyzer支持如下的過濾:/FlateDecode,/LZWDecode, /ASCII85Decode和/DCTDecode。文本文件的擴展名是.txt,二進制文件的擴展名是.bin,圖片文件的擴展名是.jpg和.bmp,字體文件的擴展名是.ttf,交叉引用文件的擴展名是.xref。/FlateDecode是ZLib Deflate壓縮算法。
下一步是構(gòu)建頁的內(nèi)容。程序跟隨從根開始的頁面樹。頁對象不是流對象。換句話說,頁描述命令是不能直接在也對象中的。頁對象字典有/Contents的鍵值對。如果不存在這個鍵值對,那么頁面就是空的。內(nèi)容入口值可以是一個單獨的引用或者是一個應(yīng)用數(shù)組。程序?qū)閬碜杂谝粋€或多個內(nèi)容流的頁面創(chuàng)建虛擬的內(nèi)容流。頁內(nèi)容虛擬流保存在PageObj_xx.txt和PageSource_xx.txt中。PageObj_xx.txt是頁面的實際描述內(nèi)容。PageSource_xx.txt是將頁面的描述內(nèi)容轉(zhuǎn)換為偽C#源代碼。在第二節(jié)概要中,有這兩個文件的例子。
頁內(nèi)容流是由參數(shù)和操作符組成的。例如矩形由四個實數(shù)描述的,內(nèi)嵌的圖片不遵循這個規(guī)則。它的描述是在第三節(jié)對象定義中。
最后,程序產(chǎn)生對象匯總文件ObjectSummary.txt。文件顯示所有簡介對象的信息不包含流。
8. PdfFileAnalyzer 程序
開發(fā)應(yīng)用程序 PdfFileAnalyzer 的目的是用來測試這個 PDF 文件解析類。如果你想在開發(fā)環(huán)境之外測試它的可執(zhí)行程序,需創(chuàng)建一個名為 PdfFileAnalyzer 的目錄并復制 PdfFileAnalyzer.exe 到這個目錄中,然后運行這個程序。如果你想從 Visual C# 開發(fā)環(huán)境中運行這個項目,請確保你在“項目屬性”的“Debug”標簽欄中定義了一個工作目錄。此程序是使用 Microsoft Visual C# 2012 開發(fā)的。
運行程序,可用的操作項有: Open, Setup 和 Exit.
程序首次執(zhí)行時你必須使用 Setup 定義工程目錄。這個目錄盛放所有被分析的 PDF 文件所產(chǎn)生的對應(yīng)子目錄。
Open 按鈕會顯示一個標準的文件選擇對話框,你可以在其中找到你要進行分析的 PDF 文件。
PDF文件分析器界面將切換到類的匯總界面:
每行代表一個間接的PDF對象。每列是:
- Object No. 間接對象號。對于附件字典來說dummy號,對象號是一個,對象號是負數(shù)時,在界面上顯示為TRn
- Ojbect 在第4節(jié)中定義的對象類型
- Type 如果對象是字典或者流,類型是/Type字典的值。如果類型不是字典或者字典不包含/Type,顯示值來自于對這個對象的間接引用
- Subtype 如果對象是字典或者流,或者字典包含/Subtype,將顯示在這一列
- Parent Object No. 如果間接對象是對象流的一部分(見4.2節(jié)復合對象),這一列顯示流對象的對象號
- Parent Index 如果間接對象是對象流的一部分,索引號是父對象流的號
- File Name 流對象和頁面對象存在文件名。File Name是文件存儲在流對象內(nèi)的名字。文件有如下的擴展名:.txt是文本文件,.bin是二進制文件,.bmp是圖片,.jpg是圖片,.ttf是字體,.xref是多引用流。如果分析MyFile.PDF的流文件,工程目錄的子目錄MyFile將被指定在啟動界面上。頁面對象不是流。文件表示這一頁所有對象的關(guān)聯(lián)關(guān)系
- Ojbect Position 如果間接對象文件不是對象流類型,這是對象在PDF文件內(nèi)的位置。如果間接對象是對象流的一部分,這對象在父對象內(nèi)的位置。位置按照十進制和十六進制數(shù)字顯示,便于程序員再二進制編輯器中查看PDF文件
- Stream Position 和 Stream Length 流的位置和長度。流的位置是相對于文件或者父對象的,同對象的位置使用相同的計算方法
點擊Summary按鈕,查看ObjectSummary.txt 文件。
選擇一行并點擊View按鈕或者雙擊一行后將顯示對象分析界面,用于查看間接對象的詳情。
對于所有的非流對象,前面的三個按鈕是不能點擊的。僅僅顯示對象自身的信息。你能用文本方式或者十六進制格式查看這些信息。
對于流對象,第一個按鈕的名字是object type。前兩個按鈕object type和Stream允許你在查看對象和流之間切換。Hex和Text按鈕允許你采用二進制格式或者文本格式查看。如果是圖片流,文本格式顯示為四列:(1) 對象號,(2) 類型 (0-未使用,1-普通對象,2-流對象),(3)普通對象的位置和流對象的父對象,(4) 父對象的索引號。如果是二進制流(例如:字體),則僅能用十六進制格式查看。
頁面對象按照流對象來處理。所有內(nèi)容對象的文本顯示是關(guān)聯(lián)的。另外,Source按鈕允許你查看頁面在C#代碼中的描述語言。
JPG圖片和BMP圖片可以旋轉(zhuǎn)方向和調(diào)整大小。
相關(guān)文章
關(guān)于C#中使用Oracle存儲過程返回結(jié)果集的問題
Oracle中可以使用游標(Cursor)對數(shù)據(jù)集進行操作,但在存儲過程輸出參數(shù)中直接使用Cursor錯誤,下面小編給大家?guī)砹薈#中使用Oracle存儲過程返回結(jié)果集的問題,感興趣的朋友一起看看吧2021-10-10WPF TextBox實現(xiàn)按字節(jié)長度限制輸入功能
這篇文章主要為大家詳細介紹了WPF TextBox實現(xiàn)按字節(jié)長度限制輸入功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11