顯示具有 C# 標籤的文章。 顯示所有文章
顯示具有 C# 標籤的文章。 顯示所有文章

CKIP Client for .NET

12 comments
CKIP 中文斷詞系統是由中文詞知識庫小組(Chinese Knowledge Information Processing Group,CKIP)所發展的線上斷詞服務。此服務採用 XML 資料交換模式,用戶端必須自行撰寫程式經由 TCP Scoket 傳送文本到斷詞系統,並剖析回傳包含斷詞及詞類標記的 XML 處理結果。

目前,CKIP Client 開放原始碼專案已實作 Java、PHP 兩種版本,可以用來簡化 CKIP 斷詞服務用戶端應用程式的開發,而不需自行撰寫 Socket 程式碼及處理 XML 資料。而本文的 CKIP Client for .NET 則是自己重新以 C# 實作 CLR 非同步程式撰寫模型(Asynchronous Programming Model)所設計的斷詞服務用戶端 APIs。

使用範例
下列範例會使用 CkipClient 在同步封鎖模式進行連接、傳送文本,並接收傳回結果,其中 username 及 password 為用戶端所申請之帳號及密碼。
using (CkipClient client = new CkipClient("username", "password"))
{
client.Send("菩提本無樹,明鏡亦非臺;本來無一物,何處惹塵埃?");
SegmentationResult result = client.GetResult();

if (result.StatusCode == StatusCode.Success)
{
StringBuilder sb = new StringBuilder();
foreach (Term term in result.GetTerms())
{
sb.Append(term.ToString() + " ");
}
Console.WriteLine(sb.ToString());
}
else
{
Console.WriteLine(string.Format("Error: {0}", result.StatusDescription));
}
}

輸出結果如下:
菩提(N) 本無樹(N) ,(COMMACATEGORY) 明鏡(N) 亦(ADV) 非(Vt) 臺(N) ;(SEMICOLONCATEGORY) 本來(ADV) 無(Vt) 一(DET) 物(N) ,(COMMACATEGORY) 何處(N) 惹(Vt) 塵埃(N) ?(QUESTIONCATEGORY)


非同步 API 使用範例
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Windows.Forms;
using CKIP;

namespace AsyncCkipClient
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}

private void uxSend_Click(object sender, EventArgs e)
{
CkipClient client = new CkipClient();
client.BeginConnect(uxUsername.Text, uxPassword.Text,
new AsyncCallback(EndConnectCallback), client);
DisplayStatus("Connecting to server ...");
}

private void EndConnectCallback(IAsyncResult ar)
{
CkipClient client = (CkipClient)ar.AsyncState;

try
{
client.EndConnect(ar);

if (client.Connected)
{
client.BeginSend(uxRawText.Text,
new AsyncCallback(EndSendCallback), client);
DisplayStatus("Sending data to server ...");
}
else
{
DisplayStatus(string.Format("Ready (last error: {0})", "Connect Failed!"));
}
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

private void EndSendCallback(IAsyncResult ar)
{
CkipClient client = (CkipClient)ar.AsyncState;

try
{
client.EndSend(ar);
client.BeginGetResult(new AsyncCallback(EndGetResultCallback), client);
DisplayStatus("Reading server response ...");
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

private void EndGetResultCallback(IAsyncResult ar)
{
CkipClient client = (CkipClient)ar.AsyncState;
try
{
SegmentationResult result = client.EndGetResult(ar);

if (result.StatusCode == StatusCode.Success)
{
StringBuilder sb = new StringBuilder();
foreach (Term term in result.GetTerms())
{
sb.Append(term.ToString() + " ");
}
DisplayResults(sb.ToString());
}
else
{
DisplayStatus(string.Format("Ready (last error: {0})",
result.StatusDescription));
}
}
catch (Exception ex)
{
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
finally
{
client.Close();
}
}

public void DisplayResults(string text)
{
if (InvokeRequired)
{
BeginInvoke(new Action<string>(DisplayResults), text);
return;
}
uxResult.Text = text;
DisplayStatus("Done");
}

public void DisplayStatus(string text)
{
if (InvokeRequired)
{
BeginInvoke(new Action<string>(DisplayStatus), text);
return;
}
uxStatus.Text = text;
}
}
}



Download source code

參考資料:
實作 CLR 非同步程式撰寫模型 (Programming Model)

繼續閱讀...

實作非同步模式的 TCP 用戶端

2 comments
TcpClient 類別是以 Socket 類別為基礎所建立的抽象層 TCP 服務,它提供簡單的方法以連接、傳送和接收網路間的資料,用來簡化 TCP 用戶端應用程式的開發。TcpClient 提供同步與非同步兩種通訊模式,本文將會著重探討如何使用非同步程式撰寫模型來處理網路服務要求。

同步模式
在一般的情況下我們都使用同步的封鎖模式來建立 Socket 應用程式,因為這種方式最簡單也最直接。以下範例將會以同步作業的方式發送 HTTP 請求:
using (TcpClient client = new TcpClient("www.msn.com", 80))
{
using (NetworkStream stream = client.GetStream())
{
byte[] send = Encoding.UTF8.GetBytes("GET HTTP/1.0 \r\n\r\n");
stream.Write(send, 0, send.Length);
byte[] bytes = new byte[client.ReceiveBufferSize];
int bytesRead = stream.Read(bytes, 0, (int)client.ReceiveBufferSize);
String data = Encoding.UTF8.GetString(bytes);
char[] unused = { (char)data[bytesRead] };
Console.WriteLine(data.TrimEnd(unused));
}
}

在上例中,我們使用 TcpClient 的建構式進行同步的連接嘗試,並透過 Stream 通訊端同步收發網路資料。在這段時間內,執行同步要求的執行緒會因為等候網路作業完成而無法再執行其他工作。如果該執行緒是 UI 執行緒,那麼應用程式就被封鎖並停止回應使用者的輸入。雖然使用同步封鎖的 Socket 從觀念上來說比較容易使用,然而如果你的應用程式需要保持快速回應、提升延展性及可靠性,那麼就不應該使用同步封鎖的作業方式。

非同步模式
非同步用戶端通訊端使用標準 .NET Framework 非同步程式撰寫模型,讓你的應用程式不會因為等候同步作業完成的執行緒而被封鎖。因為非同步作業(Asynchronous Operation)會在不同的執行緒上(在背景中)執行,所以應用程式可以在呼叫非同步方法(BeginOperationName)的執行緒上繼續執行指令。例如,在 Windows 應用程式中,非同步執行作業將會委派給背景執行緒,而使用者介面執行緒(在前景中)在作業執行時仍可以保持回應狀態。


接下來,我們將以 Windows Form 應用程式來逐步建構非同步傳輸的 HTTP 用戶端程式。首先,必須先建立用來在非同步呼叫間傳送狀態資訊的狀態物件。然後,呼叫 TcpClient 物件的 BeginConnect 方法開始遠端主機連接的非同步要求。
TcpClient client = new TcpClient();
StateObject state = new StateObject(client);
state.Data.AppendFormat("GET {0} HTTP/1.0\r\n", url);
state.Data.AppendFormat("Host:{0}\r\n\r\n", hostName);

client.BeginConnect(hostName, 0,
new AsyncCallback(EndConnectCallback), state);

當完成非同步連線要求時,系統會回呼實作 AsyncCallback 委派的 EndConnectCallback 方法。在這個方法中,我們必須呼叫 EndConnect 方法來結束擱置的非同步連接要求。下列程式碼實作了 EndConnectCallback 方法:
private void EndConnectCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;

try
{
client.EndConnect(ar);

if (client.Connected)
{
NetworkStream stream = client.GetStream();
if (stream.CanWrite)
{
byte[] send = Encoding.UTF8.GetBytes(state.Data.ToString());
stream.BeginWrite(send, 0, send.Length,
new AsyncCallback(EndWriteCallback), state);
}
}
else
{
DisplayStatus(string.Format("Ready (last error: {0})", "Connect Failed!"));
}
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

在用戶端通訊端讀寫資料時,我們需要定義一個能儲存非同步作業相關資訊的狀態物件。
internal class StateObject
{
public readonly TcpClient Client;
public readonly byte[] Buffer;
public readonly StringBuilder Data;
public readonly int BufferSize = 1024;

public StateObject(TcpClient client)
{
this.Client = client;
this.Data = new StringBuilder();
this.Buffer = new byte[this.BufferSize];
}
}

當成功連接到遠端主機後,接著會透過 Stream 通訊端的 BeginWrite 方法以非同步方式將 HTTP 請求字串寫入網路資料流,並於作業完成後回呼實作 AsyncCallback 委派的 EndWriteCallback 方法,這個方法會在網路資料流做好接收準備時接收遠端主機回應的資料。
private void EndWriteCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;

try
{
NetworkStream stream = client.GetStream();
stream.EndWrite(ar);

state.Data.Length = 0;
if (stream.CanRead)
{
stream.BeginRead(state.Buffer, 0, state.BufferSize,
new AsyncCallback(EndReadCallback), state);
}
}
catch (Exception ex)
{
client.Close();
DisplayStatus(string.Format("Ready (last error: {0})", ex.Message));
}
}

接著會呼叫 Stream 通訊端的 BeginRead 方法,以非同步方式從用戶端通訊端讀取資料,並於作業完成後回呼實作 AsyncCallback 委派的 EndReadCallback 方法,將遠端主機回應的資料讀入資料緩衝區並建立訊息字串。然後再次呼叫 BeginRead 方法,直到用戶端接收資料完畢。
private void EndReadCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
TcpClient client = state.Client;
NetworkStream stream = client.GetStream();

int bytesRead = stream.EndRead(ar);

if (bytesRead > 0)
{
state.Data.Append(Encoding.UTF8.GetString(state.Buffer, 0, bytesRead));
stream.BeginRead(state.Buffer, 0, state.BufferSize,
new AsyncCallback(EndReadCallback), state);
}
else
{
client.Close();
DisplayResults(state.Data.ToString());
}
}

最後,呼叫 DisplayResults 方法將非同步讀取作業所建立訊息字串顯示在 TextBox 控制項。
public void DisplayResults(string text)
{
if (InvokeRequired)
{
BeginInvoke(new Action<string>(DisplayResults), text);
return;
}

int hdrLen = 0;
if (-1 != (hdrLen = text.IndexOf("\r\n\r\n")))
{
uxHeader.Text = text.Substring(0, hdrLen);
char[] unused = { '\r', '\n', '\r', '\n' };
uxContent.Text = text.Substring(hdrLen).TrimStart(unused);
}
else
{
uxHeader.Text = string.Empty;
uxContent.Text = text;
}
}

因為 Windows Form 控制項的存取並沒有保證執行緒安全(Thread-safe),也就是說在當一個以上的執行緒同時存取共用資源時,就會發生競爭情形而導致狀態不一致或是死結的情況。所以,在多執行緒的表單中必須以安全執行緒的方式來存取控制項。因此,在 DisplayResults 方法中使用表單的 InvokeRequired 屬性來判斷呼叫端是否來自於建立控制項之執行緒以外的執行緒。如果是跨執行緒呼叫(Cross-Thread),則必須使用 Invoke 或是 BeginInvoke 方法來封送處理(Marshaling)至控制項的執行緒。當你在 Visual Studio 偵錯工具中執行應用程式,若有不安全的執行緒嘗試存取控制項時,偵錯工具會引發 InvalidOperationException 例外狀況:「跨執行緒作業無效: 存取控制項 control name 時所使用的執行緒與建立控制項的執行緒不同」。這也意味只有在偵錯期間才有可能發現這個例外狀況,如果應用程式完成建置與佈署,那麼即使有發生不安全的存取情況也不會引發錯誤,建議你在發現問題時加以修正。



Download sample code

參考資料:
Using an Asynchronous Client Socket
Asynchronous Client Socket Example
Asynchronous operations, pinning
以非同步的方式呼叫同步方法
使用非同步用戶端通訊端

繼續閱讀...

iTextSharp 中文字型解決方案

0 comments
iTextSharp 是由 iText(for Java)移植到 .NET 平台的開放原始碼專案。它是一個完全使用 C# 語言所撰寫的 PDF APIs,可以讓你即時建立 PDF 文件。然而,當你要使用 iTextSharp 來建立包含中文字型的文件時,就會面臨無法正常顯示中文的問題。這是因為 iTextSharp 只支援 14 個標準的 Type 1 英文字型,並沒有直接支援中文字型的顯示,取而代之的是你必須自行指定中文字型來源才能獲得解決,所以在此整理顯示中文字的幾種解決方案。

Windows 內建的中文字型
中文版 Windows 內建的中文字型檔可分為 True Type、True Type Collection 及 Open Type 三種字型格式。你可以在 %WINDIR%\Fonts 目錄下找到以 .ttf、.ttc 為延伸檔名的中文字型檔。以下範例將會使用標楷體字型來顯示文字:
Document document = new Document();
PdfWriter.GetInstance(
document,
new FileStream(@"cjk.pdf", FileMode.Create)
);
document.Open();

string fontPath = Environment.GetFolderPath(Environment.SpecialFolder.System) +
@"\..\Fonts\kaiu.ttf";
BaseFont bfChinese = BaseFont.CreateFont(
fontPath,
BaseFont.IDENTITY_H, //橫式中文
BaseFont.NOT_EMBEDDED
);
Font fontChinese = new Font(bfChinese, 16f, Font.NORMAL);
document.Add(new Paragraph(
"難得糊塗",
fontChinese
));
document.Close();

在上例中,你也可以使用 FontFactory 代替 BaseFont 來取得中文字型:
FontFactory.Register(fontPath);
Font fontChinese = FontFactory.GetFont("標楷體", BaseFont.IDENTITY_H, 16f);
document.Add(new Paragraph(
"吃虧是福",
fontChinese
));

CID 字型(Character Identity-keyed Fonts)
CID 字型格式通常被應用到中日韓(Chinese Japanese Korean, CJK)字元集。如果要使用 CJK 字型,你就必須搭配額外的亞洲語言包 iTextAsian-1.0.dll
Document document = new Document();
BaseFont.AddToResourceSearch("iTextAsian.dll");

PdfWriter.GetInstance(
document,
new FileStream(@"cjk.pdf", FileMode.Create)
);
document.Open();

BaseFont bfChinese = BaseFont.CreateFont(
"MHei-Medium",
"UniCNS-UCS2-H", // 橫式中文
BaseFont.NOT_EMBEDDED
);
Font fontChinese = new Font(bfChinese, 8);
document.Add(new Paragraph(
@"聰明難,糊塗難,
由聰明而轉入糊塗更難,
放一著,退一步,當下心安,非圖後來福報也。",
fontChinese
));
document.Close();

Download sample code

參考資料:
iText Tutorial: Getting fonts

繼續閱讀...

Printing PDF Documents using C#

0 comments
在報表應用中,PDF 是常被使用的文件格式,透過 iTextSharp 可以讓你即時(On-The-Fly)建立 PDF 文件。然而,如果你想自動將建立的文件內容輸出至印表機時,可能就會面臨苦無對策的窘境。因為,iTextSharp 並未提供實際的列印文件功能。所幸,這個問題可以藉由動態資料交換(Dynamic Data Exchange, DDE)技術獲得解決,也就是透過 DDE 從 Adobe Reader 來要求列印指定的文件檔案。在 .NET 中,我們可以使用 NDde 這個開放源碼專案,來輕鬆撰寫 DDE 連結的功能。

在接下來的程式碼範例中,除了參考「Printing PDF documents in C#」的做法外,同時亦採用 Clifton Nock 所著「Data Access Patterns」一書中的 Retryer Pattern。以下程式碼是實作 IRetryable 介面的類別,用來執行列印 PDF 文件的工作:
using System;
using System.Diagnostics;
using NDde;
using NDde.Client;

namespace PdfPrintExample
{
class PdfPrintOperation : IRetryable
{
private DdeClient _client;
private string _filePath;

public PdfPrintOperation(string filePath)
{
_filePath = filePath;
_client = new DdeClient("acroview", "control");
}

public bool Attemp()
{
try
{
_client.Connect();
return true;
}
catch (DdeException)
{
// ignore exception
}

return false;
}

public void Recover()
{
// try running Adobe Reader
Process p = new Process();
p.StartInfo.FileName = "AcroRd32.exe";
p.Start();
p.WaitForInputIdle();
}

public void Print()
{
_client.Execute("[DocOpen(\"" + _filePath + "\")]", 60000);
_client.Execute("[FilePrintSilent(\"" + _filePath + "\")]", 60000);
_client.Execute("[DocClose(\"" + _filePath + "\")]", 60000);
_client.Execute("[AppExit]", 60000);
}
}
}

以下是簡單的使用範例:
namespace PdfPrintExample
{
class Program
{
static void Main(string[] args)
{
PdfPrintOperation operation = new PdfPrintOperation(@"C:\sample.pdf");
Retryer retryer = new Retryer(operation);
retryer.Perform(10, 5000);
// print the file
operation.Print();
}
}
}

使用 Retryer Pattern 的好處在於連結 DDE Server 發生 DDEException 例外狀況時,可以自動嘗試重新連線,直到連接成功或是達到最大重試次數為止。

Download source code

參考資料:
Printing PDF documents in C#

繼續閱讀...

Read Fixed-Width Text Records using C#

0 comments
雖然使用純文字檔格式(Flat File)來交換資料並不是很好的方式,然而在 XML 盛行的今日它仍是普遍存在的資料交換格式。純文字檔格式的資料列通常以特定的符號區隔,或是固定寬度欄位來區隔不同資料欄位。從程式員的觀點來看,處理固定寬度欄位格式的資料並不如使用特定的符號區隔的格式來得輕鬆。如果是使用傳統的方式從「位置」擷取資料,那將會更加麻煩。所以,本文在這裡就要介紹幾種處理固定寬度格式資料的解決方案。

以下範例文字檔內容是以固定寬度格式所描述的銷售訂單內容:
0001S0000120081101       2     5000
0001S0000220081101 10 2500
0001S0000320081101 1 10500

其檔案格式內容定義如下:
欄位名稱欄位長度
銷售店代號X(4)
訂單編號X(6)
訂單日期X(8)
銷售量9(8)
銷售金額9(9)

LINQ to Text Files
首先必須先定義一個延伸方法(Extension Method)來逐行讀取資料流,並建立可以供 LINQ 查詢的可列舉集合。在此我引用了 Eric White 在 LINQ to Text Files 中所提供的 StreamReaderSequence 類別:
public static class StreamReaderSequence
{
public static IEnumerable<string> Lines(this StreamReader source)
{
String line;

if (source == null)
throw new ArgumentNullException("source");
while ((line = source.ReadLine()) != null)
{
yield return line;
}
}
}

接下來使用規則運算式(Regular expression)依序定義群組編號及資料欄位長度:
Regex re = new Regex("^(?<1>.{4})" +
"(?<2>.{6})" +
"(?<3>.{8})" +
"(?<4>.{8})" +
"(?<5>.{9})",
RegexOptions.Compiled);

接著再搭配 LINQ 語法的 let 子句取得規則運算式比對所擷取的群組集合,然後併入查詢結果中:
using (StreamReader sr = new StreamReader(@"d:\orders.txt"))
{
var orders =
from line in sr.Lines()
let fields = re.Match(line).Groups
select new
{
StoreID = fields[1].Value.Trim(),
OrderNumber = fields[2].Value.Trim(),
OrderDate = DateTime.ParseExact(fields[3].Value.Trim(),
"yyyyMMdd", CultureInfo.InvariantCulture),
Quantity = int.Parse(fields[4].Value),
Amount = int.Parse(fields[5].Value)
};

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}
}

LINQ to SQL
Microsoft OLE DB Provider for Jet 可用來存取及查詢文字檔,但必須在文字檔的相同目錄中建立 Schema.ini 檔來描述文字檔的結構:
[orders.txt]
Format=FixedLength
ColNameHeader=False
MaxScanRows=0
Col1=StoreID Text Width 4
Col2=OrderNumber Text Width 6
Col3=OrderDate DateTime Width 8
Col4=Quantity Long Width 8
Col5=Amount Long Width 9
DateTimeFormat=yyyyMMdd

然後,再定義一個資料結構對應的類別:
public class Order
{
public string StoreID;
public string OrderNumber;
public DateTime OrderDate;
public int Quantity;
public int Amount;
}

使用 DataContext 物件透過資料庫連接直接執行 SQL 查詢,並傳回資料結構對應的物件集合。
string filePath = @"d:\orders.txt";
string connString = string.Format( @"Provider=Microsoft.Jet.OLEDB.4.0;" +
"Data Source={0};" +
"Extended Properties='text;HDR=No;Format=FixedLength'",
Path.GetDirectoryName(filePath));

OleDbConnection conn = new OleDbConnection(connString);
DataContext db = new DataContext(conn);

string cmdText = string.Format("SELECT * FROM {0}",
Path.GetFileName(filePath).Replace(".", "#"));
var orders = db.ExecuteQuery<Order>(cmdText);

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}

FileHelpers Library
FileHelpers 是一個用來處理符號區隔或是固定寬度資料的免費類別庫。使用前,你首先必須先定義一個資料結構對應的類別:
[FixedLengthRecord]
public class Order
{
[FieldFixedLength(4)]
public string StoreID;

[FieldFixedLength(6)]
public string OrderNumber;

[FieldFixedLength(8)]
[FieldConverter(ConverterKind.Date, "yyyyMMdd")]
public DateTime OrderDate;

[FieldFixedLength(8)]
[FieldTrim(TrimMode.Left)]
public int Quantity;

[FieldFixedLength(9)]
[FieldTrim(TrimMode.Left)]
public int Amount;
}

然後,透過 FileHelperEngine 讀取資料檔就會自動建立對應的物件陣列。
FileHelperEngine engine = new FileHelperEngine(typeof(Order));          
Order[] orders = engine.ReadFile(@"d:\orders.txt") as Order[];

foreach (var order in orders)
{
Console.WriteLine("{0},{1},{2},{3},{4}",
order.StoreID,
order.OrderNumber,
order.OrderDate,
order.Quantity,
order.Amount
);
}


參考資料:
LINQ TO Text Files by Eric White

繼續閱讀...

使用 C# 產生網頁縮圖

0 comments
在 Web 2.0 網站中,我們常可以看到分享文章或是推薦網站的快照縮圖(Snapshot Thumbnail),來供使用者預覽。如果要在 .NET 中要實做網頁快照,你可以使用 WebBrowser 控制項來達成。在本文中將會示範如何建立 Windows 應用程式來擷取網頁快照並產生縮圖。
snap

範例程式碼
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace SnapExample
{
public partial class MainForm : Form
{
private WebPageSnapshot _wps = new WebPageSnapshot(1024, 768);

public MainForm()
{
InitializeComponent();
_wps.ImageCompleted += new ImageCompletedEventHandler(RefreshImage);
}

private void uxGo_Click(object sender, EventArgs e)
{
EnableButtons(false);
_wps.GenerateAsync(uxUrl.Text);
}

private void uxSaveAs_Click(object sender, EventArgs e)
{
uxSaveFileDialog.DefaultExt = "jpg";
uxSaveFileDialog.AddExtension = true;
uxSaveFileDialog.Filter = "JPEG(*.jpg)|*.jpg";
uxSaveFileDialog.ShowDialog(this);

string fileName = uxSaveFileDialog.FileName;
if (fileName != string.Empty)
{
uxThumbnail.BackgroundImage.Save(fileName);
}
}

private void RefreshImage(Image image)
{
decimal sizeRatio = ((decimal)image.Height / image.Width);

int thumbWidth = 100;
int thumbHeight = decimal.ToInt32(sizeRatio * thumbWidth);

Image thumb = image.GetThumbnailImage(
thumbWidth, thumbHeight,
() => false,
IntPtr.Zero
);

if (WebPageSnapshot.IsBlank((Bitmap)thumb))
{
using (Graphics g = Graphics.FromImage(thumb))
{
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
g.DrawString(
"IMAGE NOT AVAILABLE",
new Font("Tahoma", 65),
Brushes.DarkGray,
new RectangleF(0, 0, thumbWidth, thumbHeight),
format
);
}
}

uxThumbnail.BackgroundImage = thumb;

EnableButtons(true);
}

private void EnableButtons(bool enabled)
{
uxSaveAs.Enabled = enabled;
}

private void uxCancel_Click(object sender, EventArgs e)
{
_wps.CancelAsync();
}
}
}

WebPageSnapshot 是一個以 WebBrowse 類別為對象的自訂外覆類別(Wrapper Class)。透過非同步呼叫 GenerateAsync 方法,會將網頁載入到內部隱含的 WebBrowser 實例中。當 WebBrowser 完整載入網頁後,便會呼叫其 DrawToBitmap 方法建立網頁快照的點陣圖,然後引發 ImageCompleted 事件傳回快照圖。在上例中,將會由 RefreshImage 程序來處理 ImageCompleted 事件,並藉由傳入的 Image 物件的 GetThumbnailImage 方法建立縮圖影像。

不過,在測試過程中發現偶會有擷取到空白影像的情況。為了能處理此例外狀況,我從這裡找來了辨識空白圖片的程式演算法,並引用到 WebPageSnapshot 類別的 IsBlank 靜態方法。在範例中,我就對空白截圖繪製了 "IMAGE NOT AVAILABLE" 的字樣。
no image

Download source code

參考資料:
Algorithm to Detect Blank Images by Chinh Do
Event-based Asynchronous Pattern Overview by Microsoft

繼續閱讀...