真?WPF 按鈕拖動和調整大小
獨立觀察員 2020 年 8 月 29 日
手頭有個 Winform 程式,是使用動態生成按鈕,然后拖動、調整大小,以此來記錄一些坐標資料,最后保存坐標資料的,
在資料量(按鈕數量)比較小的時候是使用得挺愉快的,但是,當按鈕數上去之后,比如達到四五百個按鈕,那就比較痛苦了,具體來說就是,無論是移動視窗,還是拖動滾動條,或者是最小化視窗再還原,都會引起界面重繪,表現為按鈕一個接一個地出現,
經過實測,與電腦的性能和 GPU 都沒有關系,網上針對 Winform 這個問題的解決方案,比如開啟雙緩沖等,都大致嘗試了,并無任何起色,反而可能更糟,所以就像網友所說,這個要么不要在同一個界面上放置太多控制元件;要么使用 WPF,畢竟 WPF 采用的是 DirectX 作為底層繪圖引擎,而 Winform 則采用傳統的 GDI ,由于業務需求,不讓在界面上放置過多控制元件的方案不太可行,或者說暫未想到有什么變通的辦法,所以決定改版為 WPF 試試,
經過幾天的改造,原 Winform 版軟體的一小部分功能已改版為 WPF 版,而且成果喜人,同樣的按鈕數量,現在無論怎樣折騰,這幾百個按鈕就如同釘在了界面上一樣,不再能看到他們載入的程序了,在這個改造的程序中,我是將 Winform 版軟體中關于按鈕拖動和調整大小的代碼改造為 WPF 版的,聽上去挺簡單的,但是還是碰到了一些問題,比如 WPF 屏蔽了滑鼠左鍵的一些事件,需要額外處理一下,還有的就是關于坐標定位的一些問題了,下面將給出一些關鍵代碼,和大家相互交流學習一下,
首先,先上一道小菜,解決一下 WPF 按鈕控制元件(Button)中文字自動換行的問題,
不對,還是先看看 Demo 的界面結構吧:

其它控制元件和布局就不說了(最后會給出 Demo 地址),關鍵的是中間這個 ScrollViewer 包裹的 Canvas,我們生成的按鈕都是在這個 Canvas 上的,拖動和調整大小也是,Winform 的布局是依賴于坐標的,WPF 的布局控制元件則基本是不使用坐標定位的,甚至都不推薦指定大小,而只有 Canvas 布局控制元件保留了以坐標定位的模式,正好適合我們的需求(之前 Winform 版使用的是 Panel 控制元件),
可以看到里面我還注釋了一個 Button ,這個就是用來演示我們的 “小菜” 問題(按鈕文字自動換行)的,我們先把注釋放開,并且只保留其寬和高的設定:

可以看到當按鈕寬度窄于文本內容時,文本內容并不能進行自動換行,且 Button 控制元件并沒有相關屬性進行設定,解決方法就是在按鈕中添加 TextBlock 控制元件,然后設定其 TextWrapping 屬性,當然,這里我們不直接這樣寫,而是使用內容模板:
<Button Width="38" Height="75" ContentTemplate="{DynamicResource DataTemplateButtonWrap}">1A005</Button>
這個模板的資源放在 App.xaml 中:
<Application.Resources> <DataTemplate x:Key="DataTemplateButtonWrap" DataType="Button"> <Grid> <TextBlock TextWrapping="Wrap" Text="{TemplateBinding Content}"></TextBlock> </Grid> </DataTemplate> </Application.Resources>
TextBlock 中使用了 TemplateBinding 將 Button 的 Content “綁架” 到了自己的 Text 中,哈哈,看看效果:

至于后臺動態系結資源則是使用 SetResourceReference 方法,后面代碼里也有體現,
好了,小菜吃完了,開始吃主菜吧:
#region 成員 private Control _control; private int _btnNum = 0; #endregion /// <summary> /// 設定控制元件在Canvas容器中的位置; /// </summary> private void SetControlLocation(Control control, Point point) { Canvas.SetLeft(control, point.X); Canvas.SetTop(control, point.Y); } /// <summary> /// 添加按鈕 /// </summary> private void AddBtnHandler() { string btnContent = GetBtnContent(); Button btn = new Button { Name = "btn" + btnContent, Content = "btn" + btnContent, Width = 80, Height = 20, }; _control = btn; AddContorlToCanvas(_control); SetControlLocation(_control, new Point(163, 55)); } /// <summary> /// 添加控制元件到界面; /// </summary> /// <param name="control"></param> private void AddContorlToCanvas(Control control) { control.MouseDown += MyMouseDown; control.MouseLeave += MyMouseLeave; //_control.MouseMove += MyMouseMove; control.KeyDown += MyKeyDown; //解決滑鼠左鍵無法觸發 MouseDown 的問題; control.AddHandler(Button.MouseLeftButtonDownEvent, new MouseButtonEventHandler(MyMouseDown), true); control.AddHandler(Button.MouseMoveEvent, new MouseEventHandler(MyMouseMove), true); CanvasMain.Children.Add(control); if (control is Button) { //模板中設定按鈕文字換行(模板資源在App.xaml中); control.SetResourceReference(ContentTemplateProperty, "DataTemplateButtonWrap"); _btnNum++; } } /// <summary> /// 生成按鈕內容 /// </summary> /// <returns></returns> private string GetBtnContent() { return (_btnNum + 1).ToString().PadLeft(3, '0'); } /// <summary> /// 洗掉按鈕 /// </summary> private void DelBtnHandler() { CanvasMain.Children.Remove(_control); }
上面代碼是對按鈕生成、添加到界面的一些操作邏輯,每個方法都有注釋,具體的大家自己看看,這里就不在贅述了,其中 添加控制元件到界面 的方法 AddContorlToCanvas 中,給控制元件(本文指的是按鈕)添加了 MouseDown、MouseLeave、MouseMove、KeyDown 等滑鼠鍵盤事件,然后開頭說過,WPF 屏蔽了 Button 的滑鼠左鍵的一些事件,所以需要使用 AddHandler 進行處理,
下面來看看主菜中的精華:
#region 實作表單內的控制元件拖動 const int Band = 5; const int BtnMinWidth = 10; const int BtnMinHeight = 10; private EnumMousePointPosition _enumMousePointPosition; private Point _point; //記錄滑鼠上次位置; #region btn按鈕拖動 /// <summary> /// 滑鼠按下 /// </summary> private void MyMouseDown(object sender, MouseEventArgs e) { //選擇當前的按鈕 Button button = (Button)sender; _control = button; //Point point = e.GetPosition(CanvasMain); //左鍵點擊按鈕后可按WSAD進行上下左右移動; if (e.LeftButton == MouseButtonState.Pressed) { button.KeyDown += new KeyEventHandler(MyKeyDown); } double left = Canvas.GetLeft(_control); double top = Canvas.GetTop(_control); //右鍵點擊按鈕可向選定方向生成新按鈕; if (e.RightButton == MouseButtonState.Pressed) { Button btn = new Button { Name = "btn" + GetBtnContent(), Content = GetStrEndNumAddOne(button.Content.ToString()) }; CheckRepeat(btn.Content.ToString()); btn.Width = _control.Width; btn.Height = _control.Height; if (rbUpper.IsChecked == true)//上 { int h = txtUpper.Text.Trim() == "" ? 0 : Convert.ToInt32(txtUpper.Text.Trim()); SetControlLocation(btn, new Point(left, top - _control.Height - h)); } if (rbLower.IsChecked == true)//下 { int h = txtLower.Text.Trim() == "" ? 0 : Convert.ToInt32(txtLower.Text.Trim()); SetControlLocation(btn, new Point(left, top + _control.Height + h)); } if (rbLeft.IsChecked == true)//左 { int w = txtLeft.Text.Trim() == "" ? 0 : Convert.ToInt32(txtLeft.Text.Trim()); SetControlLocation(btn, new Point(left - _control.Width - w, top)); } if (rbRight.IsChecked == true)//右 { int w = txtRight.Text.Trim() == "" ? 0 : Convert.ToInt32(txtRight.Text.Trim()); SetControlLocation(btn, new Point(left + _control.Width + w, top)); } _control = btn; AddContorlToCanvas(_control); } //TODO 中鍵點擊按鈕可進行資訊編輯; } /// <summary> /// 檢查重復內容按鈕 /// </summary> /// <param name="content"></param> private void CheckRepeat(string content) { foreach (Control c in CanvasMain.Children) { if (c is Button btn) { if (content == btn.Content.ToString()) { MessageBox.Show("出現重復按鈕內容:" + content, "提示"); return; } } } } /// <summary> /// 獲取非純數字字串的數值加一結果; /// </summary> private string GetStrEndNumAddOne(string str) { int numberIndex = 0; //數字部分的起始位置; int charIndex = 0; foreach (char tempchar in str.ToCharArray()) { charIndex++; if (!char.IsNumber(tempchar)) { numberIndex = charIndex; } } string prefix = str.Substring(0, numberIndex); string numberStrOrigin = str.Remove(0, numberIndex); string numberStrTemp = ""; if (numberStrOrigin != "") { numberStrTemp = (Convert.ToInt32(numberStrOrigin) + 1).ToString(); } string result = ""; if (numberStrOrigin.Length <= numberStrTemp.Length) { result = prefix + numberStrTemp; } else { result = prefix + numberStrTemp.PadLeft(numberStrOrigin.Length, '0'); } return result; } /// <summary> /// 滑鼠離開 /// </summary> private void MyMouseLeave(object sender, EventArgs e) { _enumMousePointPosition = EnumMousePointPosition.MouseSizeNone; _control.Cursor = Cursors.Arrow; } /// <summary> /// 滑鼠移動 /// </summary> private void MyMouseMove(object sender, MouseEventArgs e) { _control = (Control)sender; double left = Canvas.GetLeft(_control); double top = Canvas.GetTop(_control); Point point = e.GetPosition(CanvasMain); double height = _control.Height; double width = _control.Width; if (e.LeftButton == MouseButtonState.Pressed) { switch (_enumMousePointPosition) { case EnumMousePointPosition.MouseDrag: SetControlLocation(_control, new Point(left + point.X - _point.X, top + point.Y - _point.Y)); break; case EnumMousePointPosition.MouseSizeBottom: height += point.Y - _point.Y; break; case EnumMousePointPosition.MouseSizeBottomRight: width += point.X - _point.X; height += point.Y - _point.Y; break; case EnumMousePointPosition.MouseSizeRight: width += point.X - _point.X; break; case EnumMousePointPosition.MouseSizeTop: SetControlLocation(_control, new Point(left, top + point.Y - _point.Y)); height -= (point.Y - _point.Y); break; case EnumMousePointPosition.MouseSizeLeft: SetControlLocation(_control, new Point(left + point.X - _point.X, top)); width -= (point.X - _point.X); break; case EnumMousePointPosition.MouseSizeBottomLeft: SetControlLocation(_control, new Point(left + point.X - _point.X, top)); width -= (point.X - _point.X); height += point.Y - _point.Y; break; case EnumMousePointPosition.MouseSizeTopRight: SetControlLocation(_control, new Point(left, top + point.Y - _point.Y)); width += (point.X - _point.X); height -= (point.Y - _point.Y); break; case EnumMousePointPosition.MouseSizeTopLeft: SetControlLocation(_control, new Point(left + point.X - _point.X, top + point.Y - _point.Y)); width -= (point.X - _point.X); height -= (point.Y - _point.Y); break; default: break; } //記錄游標拖動到的當前點 _point.X = point.X; _point.Y = point.Y; if (width < BtnMinWidth) width = BtnMinWidth; if (height < BtnMinHeight) height = BtnMinHeight; _control.Width = width; _control.Height = height; } else { _enumMousePointPosition = GetMousePointPosition(_control, e); //'判斷游標的位置狀態 switch (_enumMousePointPosition) //'改變游標 { case EnumMousePointPosition.MouseSizeNone: _control.Cursor = Cursors.Arrow; //'箭頭 break; case EnumMousePointPosition.MouseDrag: _control.Cursor = Cursors.SizeAll; //'四方向 break; case EnumMousePointPosition.MouseSizeBottom: _control.Cursor = Cursors.SizeNS; //'南北 break; case EnumMousePointPosition.MouseSizeTop: _control.Cursor = Cursors.SizeNS; //'南北 break; case EnumMousePointPosition.MouseSizeLeft: _control.Cursor = Cursors.SizeWE; //'東西 break; case EnumMousePointPosition.MouseSizeRight: _control.Cursor = Cursors.SizeWE; //'東西 break; case EnumMousePointPosition.MouseSizeBottomLeft: _control.Cursor = Cursors.SizeNESW; //'東北到南西 break; case EnumMousePointPosition.MouseSizeBottomRight: _control.Cursor = Cursors.SizeNWSE; //'東南到西北 break; case EnumMousePointPosition.MouseSizeTopLeft: _control.Cursor = Cursors.SizeNWSE; //'東南到西北 break; case EnumMousePointPosition.MouseSizeTopRight: _control.Cursor = Cursors.SizeNESW; //'東北到南西 break; default: break; } } } /// <summary> /// 按鍵WSAD(上下左右) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MyKeyDown(object sender, KeyEventArgs e) { double left = Canvas.GetLeft(_control); double top = Canvas.GetTop(_control); switch (e.Key) { case Key.W://上 { SetControlLocation(_control, new Point(left, top-1)); break; } case Key.S://下 { SetControlLocation(_control, new Point(left, top+1)); break; } case Key.A://左 { SetControlLocation(_control, new Point(left-1, top)); break; } case Key.D://右 { SetControlLocation(_control, new Point(left+1, top)); break; } } } #endregion 按鈕拖動 #region 滑鼠位置 /// <summary> /// 滑鼠指標位置列舉; /// </summary> private enum EnumMousePointPosition { /// <summary> /// 無 /// </summary> MouseSizeNone = 0, /// <summary> /// 拉伸右邊框 /// </summary> MouseSizeRight = 1, /// <summary> /// 拉伸左邊框 /// </summary> MouseSizeLeft = 2, /// <summary> /// 拉伸下邊框 /// </summary> MouseSizeBottom = 3, /// <summary> /// 拉伸上邊框 /// </summary> MouseSizeTop = 4, /// <summary> /// 拉伸左上角 /// </summary> MouseSizeTopLeft = 5, /// <summary> /// 拉伸右上角 /// </summary> MouseSizeTopRight = 6, /// <summary> /// 拉伸左下角 /// </summary> MouseSizeBottomLeft = 7, /// <summary> /// 拉伸右下角 /// </summary> MouseSizeBottomRight = 8, /// <summary> /// 滑鼠拖動 /// </summary> MouseDrag = 9 } /// <summary> /// 獲取滑鼠指標位置; /// </summary> /// <param name="control"></param> /// <param name="e"></param> /// <returns></returns> private EnumMousePointPosition GetMousePointPosition(Control control, MouseEventArgs e) { Size size = control.RenderSize; Point point = e.GetPosition(control); Point pointCanvas = e.GetPosition(CanvasMain); _point.X = pointCanvas.X; _point.Y = pointCanvas.Y; if ((point.X >= -1 * Band) | (point.X <= size.Width) | (point.Y >= -1 * Band) | (point.Y <= size.Height)) { if (point.X < Band) { if (point.Y < Band) { return EnumMousePointPosition.MouseSizeTopLeft; } else { if (point.Y > -1 * Band + size.Height) { return EnumMousePointPosition.MouseSizeBottomLeft; } else { return EnumMousePointPosition.MouseSizeLeft; } } } else { if (point.X > -1 * Band + size.Width) { if (point.Y < Band) { return EnumMousePointPosition.MouseSizeTopRight; } else { if (point.Y > -1 * Band + size.Height) { return EnumMousePointPosition.MouseSizeBottomRight; } else { return EnumMousePointPosition.MouseSizeRight; } } } else { if (point.Y < Band) { return EnumMousePointPosition.MouseSizeTop; } else { if (point.Y > -1 * Band + size.Height) { return EnumMousePointPosition.MouseSizeBottom; } else { return EnumMousePointPosition.MouseDrag; } } } } } else { return EnumMousePointPosition.MouseSizeNone; } } #endregion 滑鼠位置 #endregion 實作表單內的控制元件拖動
俗話說,Talk is cheap,show me the code,那么既然代碼已給出,大家就直接批評指正唄,我也沒什么說的了(主要是肚子餓了),
給個效果圖吧:

動圖:

最后給出 Demo 地址:
https://gitee.com/dlgcy/Practice/tree/master/WPFPractice
同步首發:
http://dlgcy.com/real-wpf-button-drag-and-resize/
微信公眾號
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/1742.html
標籤:WPF
