WPF 按钮拖动和调整大小

独立观察员 2020 年 8 月 29 日

手头有个 Winform 程序,是使用动态生成按钮,然后拖动、调整大小,以此来记录一些坐标数据,最后保存坐标数据的。

在数据量(按钮数量)比较小的时候是使用得挺愉快的,但是,当按钮数上去之后,比如达到四五百个按钮,那就比较痛苦了。具体来说就是,无论是移动窗口,还是拖动滚动条,或者是最小化窗口再还原,都会引起界面重绘,表现为按钮一个接一个地出现。

经过实测,与电脑的性能和 GPU 都没有关系,网上针对 Winform 这个问题的解决方案,比如开启双缓冲等,都大致尝试了,并无任何起色,反而可能更糟。所以就像网友所说,这个要么不要在同一个界面上放置太多控件;要么使用 WPF,毕竟 WPF 采用的是 DirectX 作为底层绘图引擎,而 Winform 则采用传统的 GDI 。由于业务需求,不让在界面上放置过多控件的方案不太可行,或者说暂未想到有什么变通的办法,所以决定改版为 WPF 试试。

经过几天的改造,原 Winform 版软件的一小部分功能已改版为 WPF 版,而且成果喜人,同样的按钮数量,现在无论怎样折腾,这几百个按钮就如同钉在了界面上一样,不再能看到他们载入的过程了。在这个改造的过程中,我是将 Winform 版软件中关于按钮拖动和调整大小的代码改造为 WPF 版的,听上去挺简单的,但是还是碰到了一些问题,比如 WPF 屏蔽了鼠标左键的一些事件,需要额外处理一下,还有的就是关于坐标定位的一些问题了,下面将给出一些关键代码,和大家相互交流学习一下。

首先,先上一道小菜,解决一下 WPF 按钮控件(Button)中文字自动换行的问题。

不对,还是先看看 Demo 的界面结构吧:

WPF 按钮拖动和调整大小

其它控件和布局就不说了(最后会给出 Demo 地址),关键的是中间这个 ScrollViewer 包裹的 Canvas,我们生成的按钮都是在这个 Canvas 上的,拖动和调整大小也是。Winform 的布局是依赖于坐标的,WPF 的布局控件则基本是不使用坐标定位的,甚至都不推荐指定大小,而只有 Canvas 布局控件保留了以坐标定位的模式,正好适合我们的需求(之前 Winform 版使用的是 Panel 控件)。

可以看到里面我还注释了一个 Button ,这个就是用来演示我们的 “小菜” 问题(按钮文字自动换行)的。我们先把注释放开,并且只保留其宽和高的设置:

WPF 按钮拖动和调整大小

可以看到当按钮宽度窄于文本内容时,文本内容并不能进行自动换行,且 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 中,哈哈。看看效果:

WPF 按钮拖动和调整大小

至于后台动态绑定资源则是使用 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。那么既然代码已给出,大家就直接批评指正呗,我也没什么说的了(主要是肚子饿了)。

给个效果图吧:

WPF 按钮拖动和调整大小

动图:

WPF 按钮拖动和调整大小

最后给出 Demo 地址:

https://gitee.com/dlgcy/Practice/tree/master/WPFPractice

原文出处:微信公众号【独立观察员赞赏账户 独立观察员博客】

原文链接:https://mp.weixin.qq.com/s/oEANNDAZCneAoVKEmQ8nng

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论

评论列表(1条)

  • ghsoft
    ghsoft 2020年9月3日 11:57

    要是在添加一个鼠标画出矩形控件的效果就好啦,关键词:Adorner