來翻新老專案 (2) - MapleLooker - WinForm 遷移到 WPF
既上次升級 .NET Core 之後
這次打算把專案的 WinForm 改用 WPF 來呈現
這算是我第一次正式寫 WPF,之前都只有開個專案拉幾個 Control 而已
原本想說應該不難,寫之後才發現事情沒我想的那麼簡單 🙃
為什麼選 WPF?
本來想看轉 WPF 後能不能脫離 Visual Studio 改用 Visual Studio Code,然而我需要的 Designer 功能依然只有 Visual Studio 可以使用…
最初本來考慮使用 UWP + WinUI3,但是 WinUI3 只能 Hot Reload ,沒辦法用 Designer ,而 WinUI2 給我的感覺並沒有 WinUI3 來得好
後來在網路上找到了 WPF UI 這東西,想說還不錯,就使用 WPF + WPF UI 了
其實我並不清楚 WPF 跟 UWP 的差別,我以為基底都是 WPF,但從官方文件來看,兩者只能說是相似而不是一樣的東西
WinForm 遷移 Wpf
如果開新專案可能還比較簡單,但我是選擇原地升級 Wpf,因此需要做一些處理把專案的 WPF 啟用,並慢慢的將 WinForm 跟 UserControl 轉成 WPF 形式
由於 WinForm 的操作邏輯與 Wpf 差異較大
所以這等於是將原本大量操作 UI 的程式碼進行大改,過程有些痛苦…
專案啟用 Wpf
在專案的 .csproj
檔案裡需要使用 UseWPF
開啟 WPF 功能
<Project Sdk="Microsoft.NET.Sdk"> |
Application 遷移
Wpf 跟 WinForm 不同的點在於:傳統 WinForm 使用最原始的 Main 作為進入點,並以 Application 為基礎掛上 MainForm 啟動應用程式。
而 Wpf 的部份,根據我自己從範本建立的專案,它會使用 xaml 來描述 Application 本身,並且不需要指定 Main 進入點
下面是原本 Form 的進入點:
using System; |
而 Wpf 則變成以 App.xaml 來設計應用程式的基底,並另外撰寫 C# 程式來開啟視窗:
<Application |
Application 啟動事件的定義
using System; |
撰寫好 App.xaml
之後,把 Program.cs
刪掉並且移除 Startup 類別便可以改啟動 Wpf 應用程式
目前的初始化方式是透過觀察 Wpf 專案範本刻出來的
不知道有沒有其他初始化 Application 的方式
Form、UserControl 遷移
這個階段主要是重刻一個 Wpf 的 UI ,並將以前的事件逐一遷移到新的 UI 上面
Wpf 的佈局上會比起以前 WinForm 提供的佈局更有彈性一些
一開始拉會有點卡卡的,習慣之後,其實設計起來會比 WinForm 還要輕鬆,呈現的效果以及響應也比 WinForm 來得好
除了拉控制項的部份有變化以外,對控制項的處理也有很大的變化
控制項變數
WinForm 的控制項打從拉進去的時候就已經自動依照序號命名變數名稱,比如 textBox1
而 Wpf 則是要透過 x:Name
這個屬性來為控制項命名變數名稱,比如:
<TextBox x:Name="textBox1"></TextBox> |
如果不使用這個屬性,一般是沒辦法直接存取控制項本身
資料綁定
原本使用 WinForm 的寫法是手動去修改控制項的資料做內容更新
而 Wpf 主要則比較常使用資料綁定的方式來動態呈現資料在介面上
WinForm 其實也有資料綁定的設計,只是我以前根本沒碰這塊
當時主要還是以手動修改控制項的資料為主
我覺得初學 Wpf 困難的地方在於資料綁定
這部份我覺得文件不太好翻閱,很常找到非常老舊的資料,或是找到 MVVM (Model-View-ViewModel) 架構的資料
目前主要是透過探索 Github 上一些有使用 Wpf 的專案才大概知道有什麼使用方式…
依照目前的了解,大概知道下面幾種綁定的基礎用法:
- Binding
- Resource
- DataContext
Binding
Binding 是最基礎的綁定單位,可以將某個物件綁定到特定屬性上
常見綁定的方式有 OnyWay, TwoWay 等等
這部份我目前只知道一些基礎的用法,像是綁定相對資源,綁定元素
舉個例子,綁定上層 Border
的 BorderBrush
:
<Border BorderBrush="Aqua"> |
透過 ElementName
,可以綁定有設定 x:Name
的元素
舉個例子,綁定元素 border1
的 BorderBrush
:
<Border x:Name="border1" BorderBrush="Aqua"> |
Resource
在 xaml 上可以為某個元素定義一些預設的資源
這些資源可以透過 StaticResource
做連接,如果是在程式上設定的 Resource,則可以透過 DynamicResource
連接
這邊以 StaticResource 做範例:
<Border> |
DataContext
DataContext 主要是提供資料的上下文,也就是提供基礎的資料來源供自己以及子元素做綁定
比如說我在 RadioButton 上綁定上下文為 checkbox1,那我可以在其他屬性直接綁定他的 IsChecked:
<CheckBox x:Name="checkbox1"> |
資料轉換
前面設定的資料綁定並不能直接寫 Code 在裡面
如果要對設定的數值做一些處理,需要經過 convertor 轉換才行
我原先有寫一個節點排序功能,可以勾選是要自動排序還是手動排序
當自動排序開啟時,手動排序的按鈕就必須反灰
為此,我寫了一個 InverseBoolConvertor
來反轉 Boolean 達到這個效果:
using System; |
<!-- 綁定 namespace `convert` --> |
設定後的效果:
事件轉移、去除設定控制項相關程式
由於用了資料綁定,因此一些手動設定資料的程式碼就可以進行移除
事件的部份,則需要更新事件方法的定義
大部分的事件參數都從 EventArgs
轉為 RoutedEventArgs
理論上以 EventArgs
作為參數的事件可以直接沿用,但我偏好使用正確的事件定義,所以全部都改成 RoutedEventArgs
TreeView 遷移
TreeView
的節點在 Wpf 上叫 TreeViewItem
而不是 TreeNode
TreeView
的 Children 也從 TreeNode
改為可以接受各種元件
較大的不同是,以前節點排序、事件、選取都是在 TreeView
設定
但現在則是在 TreeViewItem
各自設定
排序的部份則需要透過對綁定資料排序來達成,因為目前是採用手動添加控制項的方式來建立節點,所以這部份還沒完成
跨執行緒操作
以前有透過 Thread 對資料進行載入,主要是處理 TreeNode 量太大導致 UI 凍結的問題
在 Wpf 沒辦法使用 .InvokeRequired
跟 .Invoke()
來跨執行緒操作 UI
取而代之,要使用 .CheckAccess()
跟 .Dispatcher.Invoke()
目前的遷移進度
除了上面提到的項目以外,轉移的過程還踩了不少坑
像是 TrackBar 在 Wpf 等同功能 Slider 就有一些事件沒辦法遷移,需要改原本的程式。或是因為 UI 庫與 Windows 的元件類別名稱重複,導致我需要用全名來寫類別名稱
目前除了少部份功能還沒遷移完成,其他大部分事件以及 UI 都成功從 WinForm 遷移過來了 🎉
原本的 UI:
新的 UI:
結語
Wpf 比我想像的還要難寫許多,以前 WinForm 寫習慣了,轉到 Wpf 完全不知道該如何下手
專案本身轉 Wpf 是一個問題,翻了一下官方文件也沒找到相關的條目,因為專案範本預設好太多東西了,導致我完全不知道建立一個 Wpf 專案需要具備哪些東西
資料綁定比想像中還要難使用,沒辦法直接套用 Web 前端的概念
還好理解之後,寫起來還算簡單,UI 的呈現效果也比想像中好,整個程式的外在質感也提昇不少呢 🙂
目前看官方跟網路上的 Wpf 教學,都傾向使用 MVVM (Model-View-ViewModel) 架構,但依照目前的 WinForm 程式,短時間還無法以這個架構去寫 Wpf,所以目前還沒使用 MVVM 架構
UI 遷移的部份先在這裡告一個段落,接著會開始去改善以前那一堆醜醜的 Code ,除了讓程式更簡潔以外,也方便未來直接對 TreeView 做資料綁定,去除手動建立控制項的部份