昨天,老周突發其想地給大伙伴們說了一下UWP應用中計算照片面積的玩法,而且老周也表示會提供WPF版本的示例。所以,今天就給大伙們補上吧。
WPF是集成在.net框架中,屬於.net的一部分,千萬不要跟我說你學.net不學WPF,那是不對的,包括ASP.NET、WCF、WF等都是.net框架的一部分,它們在本質上並沒有脫離.net。
廢話少扯,扯了也沒人聽,咱們說正題吧。
WPF庫中與UWP的不太一樣,圖像解碼編碼API似乎不像UWP中那么強大,大概是因為桌面程序可以調用Win32 API和COM的原因吧。不過,老周必須告訴你一個事,經過測試,用WPF的方法計算照片面積,在性能上遠遠超過GDI的方式,尤其是對大型照片更是如此。在WPF出現前,在System.Drawing命名空間下有個Iamge類,也派生出了一個Bitmap類,這些類都可以用來計算照片面積。老周在N年前做過一個照片面積計算器,除了計算面積外,還可以輸入單價(每平方米多少錢),然后計算沖印照片的面積和最終的價格,並具有簡單的票據打印功能。這個程序是基於GDI,即用System.Drawing命名空間下的東東搞的,因為那個時代,WPF還沒有問世。
WPF把圖像的解碼/編碼類都內置到UI相關的媒體功能中,就是位於System.Windows.Media.Imaging命名空間下,里面有個BitmapImage類,它可以讀到照片的像素寬高,以及分辨率,有了這些參數就可以計算面積了。
但是,你必須注意WPF的線程安全模型是相對嚴格的,為了保護UI線程不被無意破壞,BitmapImage類是間接繼承DispatcherObject類,凡是有Dispatcher的對象,你得明白,它是不能跨線程操作的。如果你要在UI上顯示它們,那么位圖對象的實例必須屬於UI線程。

如果你使用數據綁定時擔心性能受影響,可以開啟異步綁定,WPF的Binding有個IsAsync屬性,開啟它,UI線程在調度時會使用輔助線程來加載數據。實驗表明,你的CPU核數越多,處理起來越快,到底還是要看配置啊。尤其是處理多媒體,像視頻這些,你不能拿一台50年前的電腦來談性能優化,你應該至少拿一台跟得上時代的電腦來評估。你總不能用春秋戰國時期的社會生產力來跟大唐盛世比。
在使用圖像時,為了節省開銷,你可以設置DecodePixelWidth或DecodePixelHeight
屬性,這兩個屬性不要同時設置,你設置其中一個就行了,這樣圖像在呈現時可以自動計算比例,不然會使圖像變形,當然了,如果你希望圖像變形,那就另當別論了。
設置這兩個屬性,並不影響我們讀取圖像的真實像素,要獲取圖像寬高,應訪問PixelWidth和PixelHeight屬性,DecodePixelHeight/Width不會影響這兩個屬性的值。另外,通過DpiX和DpiY屬性就能得到水平和垂直方向上的分辨率。
好,同樣地,還是先封裝一個數據類。
public class PhotoData : INotifyPropertyChanged { #region 私有字段 int width = default(int), height = default(int); double dpix = default(double), dpiy = default(double); BitmapImage bitmap = null; double area; #endregion #region INotifyPropertyChanged成員 public event PropertyChangedEventHandler PropertyChanged; #endregion #region 構造函數 public PhotoData(string filePath) { // 實例化圖像對象 PhotoImage = new BitmapImage(); PhotoImage.DecodePixelWidth = 100; // 初始化圖像 PhotoImage.BeginInit(); PhotoImage.UriSource = new Uri(filePath); PhotoImage.EndInit(); // 獲取需要的參數 Width = PhotoImage.PixelWidth; Height = PhotoImage.PixelHeight; DpiX = PhotoImage.DpiX; DpiY = PhotoImage.DpiY; // 計算面積 ComputeArea(); } #endregion #region 方法 private void OnPropertyChanged([CallerMemberName]string prpname = "") { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prpname)); } /// <summary> /// 計算面積 /// </summary> private void ComputeArea() { // 先將寬度和高度轉為英寸 double inchW = Width / DpiX; double inchH = Height / DpiY; /* 面積單位為平方米 1英寸 = 2.54厘米 */ Area = (inchW * 2.54d) * (inchH * 2.54d) / 10000d; } #endregion #region 屬性 /// <summary> /// 照片寬度 /// </summary> public int Width { get { return width; } private set { if (value != width) { width = value; OnPropertyChanged(); } } } /// <summary> /// 照片高度 /// </summary> public int Height { get { return height; } private set { if (value != height) { height = value; OnPropertyChanged(); } } } /// <summary> /// 水平分辨率 /// </summary> public double DpiX { get { return dpix; } private set { if (dpix != value) { dpix = value; OnPropertyChanged(); } } } /// <summary> /// 垂直分辨率 /// </summary> public double DpiY { get { return dpiy; } private set { if (value != dpiy) { dpiy = value; OnPropertyChanged(); } } } /// <summary> /// 圖像實例 /// </summary> public BitmapImage PhotoImage { get { return bitmap; } private set { if (bitmap != value) { bitmap = value; OnPropertyChanged(); } } } /// <summary> /// 面積(平方米) /// </summary> public double Area { get { return area; } private set { if (value != area) { area = value; OnPropertyChanged(); } } } #endregion }
這個我不多解釋了,應該比昨天那個更好懂。注意我今天用的單位是平方米,計算平方厘米后,要除以10000。
類似的方法,我們先計算單個照片的面積,並放入到一個集合中。
string[] files = openFileDlg.FileNames; // 開始計算 photolist.Clear(); isRunning = true; foreach (string f in files) { try { PhotoData data = new PhotoData(f); photolist.Add(data); } catch (Exception ex) { // 記錄異常信息 Trace.WriteLine($"{DateTime.Now.ToLongTimeString()} -- {ex.Message}"); continue; } }
然后,統計總面積。
double totalArea = photolist.Sum(d => { if (double.IsInfinity(d.Area)) { return 0d; } return d.Area; });
為了使這個程序更加生動可愛,更具有內涵,老周還使用了語音朗讀API,在System.Speech.Synthesis命名空間下。
// 語音朗讀 Task.Run(() => { using (SpeechSynthesizer symthedizer = new SpeechSynthesizer()) { symthedizer.Speak(msg); } });
使用Task來異讀朗讀,不影響UI上呈現內容。
最后結果請看下圖。

好了,該開飯了,今天的牛皮就吹到這里,有空繼續吹。
