顯示具有 WPF 標籤的文章。 顯示所有文章
顯示具有 WPF 標籤的文章。 顯示所有文章

2012年12月25日 星期二

WPF DataBinding 資料繫結簡介

我想有兩種狀況會有這個需求。

1.如果你需要資料跟UI元件能即時隨時的互動,隨著資料改變,UI的顯示就跟著變(或者反向),這時候,你就需要他。

2.如果你需要在Thread或Task中顯示目前進行的狀況或是任何資訊在某種UI元件上,比方Label, TextBlock,為了避免跨執行緒UI操作這種神秘又討厭的Exception,而你又不想為此作Delegate,這時候,你也可以靠他幫助。


在WPF上的作法有兩種,一種是把Bnding寫在 XML 裡,一種是寫在C#程式裡,差異不大,因為最後資料的顯示也都用程式在控制居多。

首先,建立一個WPF 應用程式專案,然後在視窗上放一個按鈕、一個TextBlock、一個TextBox。名稱Name分別取為Btn_Bind、TxtBlk_Bind、TxtBx_Bind。位置隨便你放,能看得到就好了。

接下來,在程式當中,新增一個繼承INotifyPropertyChanged的Class,並且給他一個Member(當然,如果你有多個元件要跟多組數值作Binding,這裡就可以有多個Member),固定寫法如下,當Member要寫入新值,在set當中呼叫OnPropertyChanged(),他會產生PropertyChangedEventHandler 的事件,因此可讓UI可以知道該更新顯示了。


        public class MyData : INotifyPropertyChanged
        {
            private string m_TextMsg;
            public string TextMsg
            {
                set
                {
                    if ( m_TextMsg != value )
                    {
                        m_TextMsg = value;
                        OnPropertyChanged( "TextMsg" );
                    }
                }
                get
                {
                    return m_TextMsg;
                }
            }

            public event PropertyChangedEventHandler PropertyChanged;
            public void OnPropertyChanged( string propertyName )
            {
                if ( PropertyChanged != null )
                {
                    PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
                }
            }
        }



另外準備好按鈕的事件內容
        private void Bind_Click( object sender, RoutedEventArgs e )
        {
            m_MyData.TextMsg = "New Value";
        }



然後在CS檔中宣告一個全域變數,把你剛剛的INotifyPropertyChanged類型的Class宣告好

        private MyData m_MyData ;

然後在程式進入點函式中加入底下,產生物件實體與給予初始值
            m_MyData = new MyData();
            m_MyData .TextMsg = "Origin";



以上都準備好之後,開始進行Binding




第一種方法:在XML加入Binding 片段

XML會變成類似底下這樣:

<TextBlock x:Name="TxtBlk_Bind" HorizontalAlignment="Center" Margin="134,130,136,140" TextWrapping="Wrap" VerticalAlignment="Center" Height="50" Width="247" FontSize="20" Text="{Binding Path=TextMsg }" TextAlignment="Center"/>
        <TextBox x:Name="TxtBx_Bind" HorizontalAlignment="Left" Height="44" Margin="134,210,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="248" FontSize="20" Text="{Binding Path=TextMsg }"/>
        <Button x:Name="Btn_Bind" Content="改變數值" HorizontalAlignment="Left" Height="37" Margin="134,62,0,0" VerticalAlignment="Top" Width="99" Click="Bind_Click"/>



最後用程式告訴系統,你的Binding是哪一個


            TxtBlk_Bind.DataContext = m_BindData;
            TxtBx_Bind.DataContext = m_BindData;


(能不能把上面兩段寫成XAML呢?其實是可以的,可是,寫來頗複雜,而且還不知道要怎麼讓CS引用這個變數)


如此,你的TxtBlk_Bind跟TxtBx_Bind兩個元件就跟m_BindData 內的TextMsg 有了互動
不相信?你可以按下按鈕,就會看到TxtBlk_Bind跟TxtBx_Bind元件都變成了"New Value",而如果你在TxtBx_Bind輸入任何文字,然後按下[Tab]鍵,就可以看到TxtBlk_Bind也會跟著變成你輸入的東西了。(這是因為TxtBx_Bind輸入的數值會同步變更BindData內的TextMsg 數值,然後在同步更新到TxtBlk_Bind元件。




第二種方法:完全在程式中完成Binding

XML不用改,只要在程式當中加上底下四行:


            Binding BindingTxtBlk = new Binding() { Source = m_MyData , Path = new PropertyPath( "TextMsg" ) };
            TxtBlk_Bind.SetBinding( TextBlock.TextProperty, BindingTxtBlk );

            Binding BindingTxtBx = new Binding() { Source = m_MyData , Path = new PropertyPath( "TextMsg" ) };

            TxtBx_Bind.SetBinding( TextBox.TextProperty, BindingTxtBx );







太棒了,對吧!
我喜歡第二種,因為不用動到XML,感覺也挺直覺的。

2012年2月5日 星期日

Image與BitmapImage如何用多執行緒來達成預先載入?

如果沒有實做過,會覺得應該不難,沒什麼問題,但是,真的在做的時候,會因為不夠瞭解,而遇到這個陷阱的。




首先,你當然必須在Windows上放上一個Image元件(要用程式建立也行啦),然後,在Window的Class層級宣告以下元件(因為很多函式會共用到,所以不能在函式內宣告。


        private BitmapImage BitMap;
        private FileStream FStream;
        private BackgroundWorker BgWk;


然後再需要載入圖片的時機,產生BackgroundWorker 物件來幫你做多執行緒處理(看是要在某個按鈕被按下,還是在某個Timer事件裡,都好)



            BgWk = new BackgroundWorker( );
            BgWk.DoWork += new DoWorkEventHandler( BgWk_DoWork );
            BgWk.RunWorkerCompleted += new RunWorkerCompletedEventHandler( BgWk_RunWorkerCompleted );
            BgWk.RunWorkerAsync( );





上面需要綁定兩個函式來做BackgroundWorker的事件,一個是DoWork(),是開新的Thread要做的事情,另外一個是RunWorkerCompleted(),則是在Thread忙完之後要做的事情,這裡,非常簡單,很直覺的可以想像,把BitmapImage載入圖片的那一段程式,放到DoWork(),載入完成之後在RunWorkerCompleted()這個函式裡,Image要載入這個圖,兩個函式如下



        private void BgWk_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
        {
            ImageControl.BeginInit( );
            ImageControl.Source = BitMap;
            ImageControl.EndInit( );
        }


        private void BgWk_DoWork( object sender, DoWorkEventArgs e )
        {
            // 開檔
            String FilePath = @"C:\temp\IMG001.JPG";
            FStream = new FileStream( FilePath, FileMode.Open );


            // 將資料讀入 BitmapImage 變成圖檔資源
            BitMap = new BitmapImage( );
            BitMap.BeginInit( );
            BitMap.CacheOption = BitmapCacheOption.OnLoad;
            BitMap.StreamSource = FStream;
            BitMap.EndInit( );
            BitMap.Freeze( );
            FStream.Close( );
        }







以上想法沒有問題,但是問題就在紅色粗斜體這一行,BitmapImage有個特性,就是他的載入圖片這個行為會牽涉到UI的變更,也就是說,當你沒有紅色這一行的時候,BitmapImage跟「會顯示畫面的UI」是同等的,也就是你無法跨執行緒使用他,這個BitmapImage已經屬於DoWork()這個執行緒了(為什麼?因為在函式裡有New產生物件,所以屬於次程序),當你回到RunWorkerCompleted()主執行緒的時候,無法再用他了,會引發「跨執行緒使用UI物件」的InvalidOperationException,那怎麼辦?我已經想要把UI跟Data分開了,如果BitmapImage是個UI物件,那就無法預先載入圖檔了呀,因為我又要在背景執行緒載入圖,不同執行緒卻又不能使用同一個物件,如果你用Dispatch來做,那也失去意義,因為Dispatch最後也是回歸到UI Thread來做事,一樣卡了UI,造成無反應狀態,原來,其中的關鍵就在於當你把圖檔載入之後,要呼叫Freeze()來把資源固定,就可以跟一般物件一樣,可以跨執行緒來使用。達成「讓沒有UI的繁瑣事情可以在背景執行,有UI的更新在前景進行啦」。

2012年1月11日 星期三

WPF的BitmapImage佔用資源的處理方式

我們經常使用Image控制項來載入圖片,簡單的作法,直接在IDE開發環境中,在Image的屬性頁面設定Source屬性即可。但,我們有時候會需要將圖檔動態的讀入,怎麼寫?可想到程式碼如下:

  // 開檔

  String FilePath = @"C:\temp\001 (1).jpg";
  FileStream FStream = new FileStream( FilePath, FileMode.Open );


  // 將資料讀入 BitmapImage 變成圖檔資源
  BitmapImage BitMap = new BitmapImage( );
  BitMap.BeginInit( );
  BitMap.StreamSource = FStream ;
  BitMap.EndInit( );


  // 放到 Image 元件中,顯示圖片
  ImageControl.BeginInit( );
  ImageControl.Source = BitMap;
  ImageControl.EndInit( );
   // FStream.Close( );     不可Close,不然圖片不會出來





可是這樣會遇到一個問題,載入圖檔之後,原本的圖檔會無法刪除,或被別人使用,因為資源被Stream鎖住了,如果你在程式中把FStream給Close了,那麼,執行的時候就看不到圖了(什麼機制嘛!)所以要改成底下的作法,把檔案讀到記憶體裡面


  // 開檔
  String FilePath = @"C:\temp\001 (1).jpg";
  FileStream FStream = new FileStream( FilePath, FileMode.Open );


  // 轉成 Binary
  BinaryReader BinReader = new BinaryReader( FStream );
  byte[] BinaBytes = BinReader.ReadBytes( (int)FStream.Length );


  // 資料變成 MemoryStream (如此檔案就跟此程式無關連了,可關閉、刪除)
  MemoryStream MStream = new MemoryStream( BinaBytes );
  BinReader.Close( );
  FStream.Close( );


  // 將資料讀入 BitmapImage 變成圖檔資源
  BitmapImage BitMap = new BitmapImage( );
  BitMap.BeginInit( );
  BitMap.StreamSource = MStream ;
  BitMap.EndInit( );


  // 放到 Image 元件中,顯示圖片
  ImageControl.BeginInit( );
  ImageControl.Source = BitMap;
  ImageControl.EndInit( );
  //MStream.Close( );     不可Close,不然圖片不會出來








這樣,檔案可以更動了(因為FileStream關閉啦),可是,MemoryStream一樣無法關閉,不然圖片不會出來,這樣還是沒有解決問題,根本的作法,是預先載入,設定BitmapCacheOption,這樣,就可以回到第一個作法,簡單作出來。



  // 開檔
  String FilePath = @"C:\temp\001 (1).jpg";
  FileStream FStream = new FileStream( FilePath, FileMode.Open );


  // 將資料讀入 BitmapImage 變成圖檔資源
  BitmapImage BitMap = new BitmapImage( );
  BitMap.BeginInit( );
  BitMap.StreamSource = FStream ;
  BitMap .CacheOption =  BitmapCacheOption.OnLoad;
  BitMap.EndInit( );
  FStream.Close();  



  // 放到 Image 元件中,顯示圖片
  ImageControl.BeginInit( );
  ImageControl.Source = BitMap;
  ImageControl.EndInit( );


2012年1月10日 星期二

WPF的Main去哪裡了?我怎麼取得執行參數?

由於WPF把main()藏了起來,包裝起來,在編譯過程才會產生,所以原本透過main(string[] args)來取得參數的方法變得無法使用了(或說我不知道該怎麼用),怎麼辦呢?可以透過以下的變通方式來用,雖然麻煩點,但是知道方法之後,也沒有多難。




首先,要在App.xaml.cs動手腳,增加以下程式碼,就可以取得




        // override 的目的是為了取得傳入的參數
        protected override void OnStartup( StartupEventArgs e )
        {
            public static String Args0;


            if ( e.Args != null && e.Args.Count( ) > 0 )
            {
                Args0 = e.Args[ 0 ].ToString( );
            }
            base.OnStartup( e );
        }  




然後在啟動的視窗(可以在App.xmal當中看到呼叫了哪個視窗,他就會是啟動視窗,比方如果是 StartupUri="WinProgress.xaml" 那WinProgress 就是啟動視窗 )
在該視窗程式碼(.cs)當中就可以用App.Args0取得傳入的參數了。




那參數如果有兩個呢?很簡單,就這樣子做:




        // override 的目的是為了取得傳入的參數
        protected override void OnStartup( StartupEventArgs e )
        {
            public static String Args0;
            public static String Args1;
            if ( e.Args != null && e.Args.Count( ) > 1 )
            {
                Args0 = e.Args[ 0 ].ToString( );
                Args1 = e.Args[ 1 ].ToString( );
            }
            base.OnStartup( e );
        }  




反而比較要注意的是,如果你透過程式要呼叫上面那個程式,想把參數傳進去,要怎麼透過Process()來傳多個參數呢?,要用個小技巧,Process.Start( "Filename", "Arg1 Arg2" );在Start()這個函式的第二個參數,傳入的自串中間用空格隔開,就會變成兩個參數了XD

2011年9月19日 星期一

WPF第一章

1.一個應用當中只能有一個Application物件,這個物件是看不見的,不像Window物件,看得到視窗,有標題列。

2.建立Window物件跟Application物件的順序沒有一定,只有一些原則要遵守,Application的Run必須保留在最後,因為Run方法一旦被呼叫就不會返回,直到視窗被關閉結束。

3.可以不要呼叫Window的Show方法而是直接把Window物件當作參數傳遞給Run方法,這樣Run方法會去呼叫Window物件的Show方法的。

4.Application類別具有一個Static Property名為Current,裡頭包含一個MainWindow物件,這個取用方式有個注意事項,宣告產生Application物件必須在宣告產生Windows物件之前完成,或是呼叫Run的時候傳入的Window物件,否則會得到Null reference exception。Current裡頭也包含著Windows的Collection類別,可以透過他取得所有的

5.Application類別具有數個有用的事件,大多數的事件都有對應的Protected方法可以發出事件,例如Startup事件是利用OnStartup方法所產生。當呼叫Application的Run()之後,就會立刻呼叫OnStartup方法,當Run返回時,則會立刻呼叫OnExit(),可以用這些事件的時機來作應用程式的初始化跟清除。

6.一個程式可以有多個視窗

2011年9月13日 星期二

什麼是WPF

開始年份
語言
介面
1985年
C
Windows API
1992年
C++
MFC程式庫
2001年
C#或C++
Windows Form
2006年
C#或C++
Windows Presentation Foundation


從最開始的Windows程式只能利用C語言以及Windows API,到過去幾年,開始出現最多人使用的的MFC程式庫,之後演進到受管控的(Managed Code)現代化的NET的Windows Form設計方式,這幾年則出現WPF這個新一代的設計方案。

WPF在繪圖、動態效果卓越提升,除了2D,還支援3D的效果。除了用程式語言C#開發,也能用XMAL語法來開發,新一代的Silver Light就是延續著WPF而精簡出來的新一代網頁開發工具。