.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N140314602
出刊日期:2014/2014/3/26
開發工具:Visual Studio 2013 Ultimate
版本:.NET Framework 4.5.1
在WPF應用程式之中,有可能會使用到圖檔資料,有時我們會選擇將圖檔的二進位資料儲存在資料庫資料表的二進位欄位之中。在這篇文章中,將以Step-by-Step的方式設計一個WPF應用程式,利用ADO.NET將存在硬碟的JPG檔儲存到資料庫,並將資料庫中圖片的二進位資料讀出,還原成圖檔,並顯示在畫面上。
啟動Visual Studio 2013發環境。從Visual Studio 2013選單「File」-「New」-「Project」,選取「Templates」- 「Visual C#」 程式語言-「Windows」分類,建立一個「WPF Application」專案,設定Location指定檔案存放路徑,請參考下圖所示:

圖 1:建立一個「WPF Application」專案。
新增資料庫檔案
在「Solution Explorer」視窗,專案名稱上方按下滑鼠右鍵,從快捷選單中選取「Add」-「 New Item」,加入一個「Service-based Database」項目,命名為Database1.mdf檔案,然後按下「Add」按鈕,請參考下圖所示:

圖 2:加入一個「Service-based Database」項目。
預設Service-based Database類型的資料庫是一個LocalDB 類型的檔案資料庫。我們先在資料庫之中建立一個資料表來儲存圖片資料。使用Visual Studio 2013開發工具,在「Solution Explorer」視窗選取新加入的Database1.mdf檔案,按下滑鼠右鍵,從快捷選單中選取「Open」,連結並開啟資料庫,請參考下圖所示:

圖 3:連結並開啟資料庫。
下一步新增一個PhotoTable資料表。從Visual Studio 2013-「Server Explorer」視窗 -「Database1.mdf」-「Tables」,按滑鼠右鍵,從快捷選單中選取「Add New Table」,這樣就會開啟資料表設計畫面,請參考下圖所示:

圖 4:增一個PhotoTable資料表。
在資料表設計畫面新增兩個欄位,請參考下圖所示:
· Id欄位:Data Type 設為int型別。
· Photo欄位:Data Type 設為varbinary(Max)型別,並勾選「Allow Nulls」。

圖 5:新增兩個欄位。
選取資料表設計畫面上的Id欄位,利用「Poperties」視窗,設定「Identity Specification」-「 (Is Identity) 」屬性的值為「true」,這樣Id欄位的內容就會自動編流水號,請參考下圖所示:

圖 6:設定為自動編流水號欄位。
在資料表設計畫面下方「T-SQL」區段,預設會自動顯示建立資料表的T-SQL語法,我們可以修改語法,將資料表的名稱設定為「PhotoTable」,目前的T-SQL語法看起來如下:
CREATE TABLE [dbo].[PhotoTable]
(
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
[Photo] VARBINARY(MAX) NULL
)
選取資料表設計畫面上方的「Update」按鈕,執行T-SQL語法建立資料表,請參考下圖所示:

圖 7:執行T-SQL語法建立資料表。
下一步會看到「Preview Database Updates」畫面,點選「Update Database」按鈕,就會直接執行T-SQL命令,請參考下圖所示:

圖 8:直接執行T-SQL命令。
完成之後「Server Explorer」視窗便會出現新增的資料表,請參考下圖所示:

圖 9:檢視新建立的資料表。
由於Windows Presentation Foundation沒有提供開啟檔案的對話盒,我們可以使用System.Windows.Forms組件提供的功能來達到選檔案動作。在「Solution Explorer」視窗,專案名稱上方按下滑鼠右鍵,從快捷選單中選取「Add」-「 Reference」,開啟「Reference Manager」對話盒,在右上方文字方塊輸入搜尋關鍵字「System.Windows.Forms」找到System.Windows.Forms組件之後,勾選此項目,然後按下「OK」按鈕,參考下圖所示:

圖 10:引用System.Windows.Forms組件。
設計使用者界面
接下來我們要來設計使用者界面,在MainWindow.xaml設計畫面中,加入ListBox、Button、Image與Label控制項,畫面大致安排如下,左方的方塊是ListBox;右方的方塊是Image,請參考下圖所示:

圖 11:設計WPF使用者界面。
先幫Labal取一個Name,選取設計畫面上的Label,從「Properties」視窗設定Name為「 lblFileName」,請參考下圖所示:

圖 12:設定Name。
按照相同的方式,利用「Properties」視窗設定Image的Name為「img」;ListBox 的Name為「lst」。完成後標籤看起來如下:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox x:Name="lst" HorizontalAlignment="Left" Height="248" Margin="35,38,0,0" VerticalAlignment="Top" Width="151"/>
<Button Content="Load Image" HorizontalAlignment="Left" Margin="229,38,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click" />
<Image x:Name="img" HorizontalAlignment="Left" Height="198" Margin="219,78,0,0" VerticalAlignment="Top" Width="250"/>
<Label x:Name="lblFileName" Content="Label" HorizontalAlignment="Left" Margin="229,286,0,0" VerticalAlignment="Top"/>
</Grid>
</Window>
雙擊「Load Image」按鈕產生事件處理常式,並進入到程式設計視窗,在檔案最上方,匯入System.Windows.Forms 、System.Data.SqlClient、System.Data、System.IO命名空間:
using System.Windows.Forms;
using System.Data.SqlClient;
using System.Data;
using System.IO;
在Button_Click事件處理常式之中,加入以下程式碼,利用OpenFileDialog開啟系統選取檔案的對話盒,讓使用者選取圖片,將圖片顯示在畫面Image控制項中,並利用ADO.NET將資料新增到資料庫:
private void Button_Click( object sender , RoutedEventArgs e ) {
byte[ ] fileData = null;
OpenFileDialog dlg = new OpenFileDialog( );
dlg.InitialDirectory = "c:\\";
dlg.Filter = "Image files (*.jpg)|*.jpg";
string selectedFileName = null;
if ( dlg.ShowDialog( ) == System.Windows.Forms.DialogResult.OK ) {
selectedFileName = dlg.FileName;
lblFileName.Content = selectedFileName;
BitmapImage bitmap = new BitmapImage( );
bitmap.BeginInit( );
bitmap.UriSource = new Uri( selectedFileName );
bitmap.EndInit( );
img.Source = bitmap;
}
if ( selectedFileName == null ) {
return;
}
FileStream fs = new FileStream( selectedFileName , FileMode.Open , FileAccess.Read );
fileData = new byte[ fs.Length ];
fs.Read( fileData , 0 , ( int ) fs.Length );
fs.Close( );
SqlConnection cn = new SqlConnection( @"Data Source=(LocalDB)\v11.0;AttachDbFilename=C:\temp\WpfApplication1\WpfApplication1\Database1.mdf;Integrated Security=True" );
SqlCommand cmd = new SqlCommand( "INSERT INTO PhotoTable (photo) VALUES (@photo)" , cn );
SqlParameter param1 = new SqlParameter( "@photo" , SqlDbType.Image );
param1.Value = fileData;
cmd.Parameters.Add( param1 );
cn.Open( );
cmd.ExecuteNonQuery( );
cn.Close( );
}
按F5執行這個程式,選取畫面中的「Load Image」按鈕,請參考下圖所示:

圖 13:測試執行結果。
在「Open」對話盒內,選取圖片檔案之後,按下「Open」按鈕,請參考下圖所示:

圖 14:選取圖檔。
圖片會直接呈現在畫面上,並直接儲存到資料庫,請參考下圖所示:

圖 15:圖片載入資料庫,並顯示在畫面上
檢視資料庫資料
使用Visual Studio 2013開發工具,在「Solution Explorer」視窗,選取Database1.mdf檔案,按下滑鼠右鍵,從快捷選單中選取「Open」,連結並開啟資料庫。在「Server Explorer」視窗,PhotoTable項目上面按右鍵,點選「Show Table Data」就可以看到資料庫新增的資料,請參考下圖所示:

圖 16:檢視資料庫資料。
載入資料庫資料
下一步我們希望在程式一開始執行時,就載入資料庫PhotoTable資料表中的所有資料,並將Id顯示在ListBox上,讓使用者可以點選id來查詢儲存在資料庫中的圖檔。開啟MainWindow.xaml「Design」設計畫面,選取「Window」,雙擊「Properties」視窗的閃電按鈕,切換到事件,雙擊Loaded項目,產生Window_Loaded事件處理常式,請參考下圖所示:

圖 17:產生Window_Loaded事件處理常式。
在Window_Loaded事件處理常式加入以下程式碼,查詢資料庫PhotoTable資料表中所有的id欄位值,並將結果利用資料繫結語法顯示在ListBox控制項上。
private void Window_Loaded( object sender , RoutedEventArgs e ) {
SqlConnection cn = new SqlConnection( @"Data Source=(LocalDB)\v11.0;AttachDbFilename=C:\temp\WpfApplication1\WpfApplication1\Database1.mdf;Integrated Security=True" );
SqlCommand cmd = new SqlCommand( "select id from PhotoTable" , cn );
cn.Open( );
SqlDataReader dr = cmd.ExecuteReader( );
DataTable dt = new DataTable( );
dt.Load( dr );
lst.ItemsSource = dt.AsDataView( );
lst.DisplayMemberPath = "id";
dr.Close( );
cn.Close( );
}
開啟MainWindow.xaml「Design」設計畫面,選取ListBox控制項,按一下「Properties」視窗的閃電按鈕,切換到事件,雙擊SelectionChanged項目,產生Window_Loaded事件處理常式,並加入以下程式碼,利用SelectedItem取得選到的圖片編號,利用編號查詢出資料庫圖檔的二進位資料出來。
private void lst_SelectionChanged( object sender , SelectionChangedEventArgs e ) {
int id = int.Parse( ( lst.SelectedItem as DataRowView )[ 0 ].ToString( ) );
const int BufferSize = 1024;
byte[ ] image = null;
SqlConnection cn = new SqlConnection( @"Data Source=(LocalDB)\v11.0;AttachDbFilename=C:\temp\WpfApplication1\WpfApplication1\Database1.mdf;Integrated Security=True" );
SqlCommand cmd = new SqlCommand( "select photo from PhotoTable where id=@id" , cn );
cmd.Parameters.AddWithValue( "@id" , id );
cn.Open( );
using ( SqlDataReader dr = cmd.ExecuteReader( CommandBehavior.SequentialAccess ) )
using ( MemoryStream imageStream = new MemoryStream( ) ) {
long currentIndex = 0;
byte[ ] buffer = new byte[ BufferSize ];
int bytesRead;
while ( dr.Read( ) ) {
currentIndex = 0;
bytesRead = ( int ) dr.GetBytes( 0 , currentIndex , buffer , 0 , BufferSize );
while ( bytesRead != 0 ) {
imageStream.Write( buffer , 0 , bytesRead );
currentIndex += bytesRead;
bytesRead =
( int ) dr.GetBytes( 0 , currentIndex , buffer , 0 , BufferSize );
}
}
dr.Close( );
cn.Close( );
if ( imageStream.Length > 0 ) {
image = imageStream.ToArray( );
}
//solution 1
string fn = System.IO.Path.GetTempFileName( );
FileStream s = new FileStream( fn , FileMode.Create , FileAccess.Write );
BinaryWriter b = new BinaryWriter( s );
b.Write( image );
b.Close( );
s.Close( );
BitmapImage bi = new BitmapImage( );
bi.BeginInit( );
bi.UriSource = new Uri( fn , UriKind.RelativeOrAbsolute );
bi.EndInit( );
img.Source = bi;
}
當我們把二進位資料讀出之後,需要把二進位資料轉換成圖片,關於這部分的做法有許多種,上述範例的做法是先利用Path.GetTempFileName( )方法產生一個暫存的檔案名稱,將二進位圖檔資料從資料庫取出之後,直接寫入暫存檔,接著再利用BitmapImage顯示圖檔的內容。此範例的參考執行畫面如下,選取ListBox控制項中圖檔的編號,下方的Image控制項就會顯示圖檔,請參考下圖所示:

圖 18:ListBox控制項顯示圖檔的編號。
這個解法的缺點是,將圖檔存成檔案需要進行硬碟的I/O動作,會導致執行效能較差,若沒有需要將圖檔存檔的需求,我們可以利用Steam物件直接在記憶體中將二進位資料還原成圖檔就好。例如將上述「//solution 1」這行程式碼之後的程式碼改為:
//solution 2
imageStream.Seek( 0 , SeekOrigin.Begin );
BitmapImage bi = new BitmapImage( );
bi.BeginInit( );
bi.StreamSource = imageStream;
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.EndInit( );
img.BeginInit( );
img.Source = bi;
img.EndInit( );
另一個解法是使用JpegBitmapDecoder類別來處理,將上述「//solution 2」這行程式碼之後的程式碼更改如下,也可以得到相同的效果:
//solution 3
imageStream.Seek( 0 , SeekOrigin.Begin );
JpegBitmapDecoder d = new JpegBitmapDecoder( imageStream , BitmapCreateOptions.PreservePixelFormat , BitmapCacheOption.OnLoad );
img.BeginInit( );
img.Source = d.Frames[ 0 ];
img.EndInit( );