You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinUI 3自定义组件开发:如何创建支持鼠标事件的GDI绘图自定义控件?

在WinUI 3中创建基于GDI绘图的自定义控件(支持鼠标交互)

如果你要做一个WinUI 3的自定义控件,不用UserControl组合,而是纯GDI绘图,还要支持点击、悬停、拖拽、双击这些交互,按下面的步骤来就行:

1. 建自定义控件类,继承FrameworkElement

首先定义一个继承自FrameworkElement的控件类,用来存交互状态(比如是不是悬停、有没有在拖拽):

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;

namespace WinUI3GDIControlDemo
{
    public class GDIDrawControl : FrameworkElement
    {
        // 标记交互状态
        private bool _isHovered;
        private bool _isDragging;
        private bool _isElementClicked;
        private Point _dragStartPos;
        // 自定义绘制元素的位置(示例用矩形)
        private Point _elementPos = new(100, 100);

        public GDIDrawControl()
        {
            // 绑定指针事件
            PointerEntered += GDIDrawControl_PointerEntered;
            PointerExited += GDIDrawControl_PointerExited;
            PointerPressed += GDIDrawControl_PointerPressed;
            PointerReleased += GDIDrawControl_PointerReleased;
            PointerMoved += GDIDrawControl_PointerMoved;
            DoubleTapped += GDIDrawControl_DoubleTapped;
        }
    }
}

2. 处理鼠标/指针事件

在控件里实现各个事件的逻辑,更新状态后触发重绘:

private void GDIDrawControl_PointerEntered(object sender, PointerRoutedEventArgs e)
{
    _isHovered = true;
    InvalidateVisual(); // 告诉控件要重绘了
}

private void GDIDrawControl_PointerExited(object sender, PointerRoutedEventArgs e)
{
    _isHovered = false;
    _isDragging = false;
    InvalidateVisual();
}

private void GDIDrawControl_PointerPressed(object sender, PointerRoutedEventArgs e)
{
    var pointerPoint = e.GetCurrentPoint(this);
    _dragStartPos = new((int)pointerPoint.Position.X, (int)pointerPoint.Position.Y);
    // 判断是不是点到了我们绘制的元素上(示例是100x60的矩形)
    var elementRect = new Rectangle(_elementPos.X, _elementPos.Y, 100, 60);
    _isElementClicked = elementRect.Contains(_dragStartPos);
    // 如果要拖拽时超出控件也能响应,这里可以加CapturePointer(e.Pointer)
    InvalidateVisual();
}

private void GDIDrawControl_PointerReleased(object sender, PointerRoutedEventArgs e)
{
    _isElementClicked = false;
    _isDragging = false;
    // 释放指针捕获(如果之前加了的话)
    // ReleasePointerCapture(e.Pointer);
    InvalidateVisual();
}

private void GDIDrawControl_PointerMoved(object sender, PointerRoutedEventArgs e)
{
    if (_isElementClicked)
    {
        _isDragging = true;
        var pointerPoint = e.GetCurrentPoint(this);
        var currentPos = new((int)pointerPoint.Position.X, (int)pointerPoint.Position.Y);
        // 计算偏移,更新元素位置
        _elementPos.X += currentPos.X - _dragStartPos.X;
        _elementPos.Y += currentPos.Y - _dragStartPos.Y;
        _dragStartPos = currentPos;
        InvalidateVisual();
    }
}

private void GDIDrawControl_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
    // 双击就把元素重置回初始位置
    _elementPos = new(100, 100);
    InvalidateVisual();
}

3. 重写OnRender,用GDI绘图

WinUI 3的OnRender本来用DrawingContext,但要调用GDI的话,得先拿到控件所在窗口的HDC,再用GDI+的API来画:

protected override void OnRender(Microsoft.UI.Xaml.Media.DrawingContext drawingContext)
{
    base.OnRender(drawingContext);

    // 获取当前控件所在的窗口句柄
    var currentWindow = Window.GetWindow(this);
    if (currentWindow == null) return;
    var hwnd = WindowNative.GetWindowHandle(currentWindow);
    
    // 从HDC创建GDI的Graphics对象
    using var g = Graphics.FromHdc(GetDC(hwnd));
    g.SmoothingMode = SmoothingMode.AntiAlias; // 开抗锯齿,画出来更顺

    // 画控件背景:悬停时变浅蓝,平时浅灰
    var bgBrush = _isHovered ? Brushes.LightBlue : Brushes.LightGray;
    g.FillRectangle(bgBrush, 0, 0, (int)ActualWidth, (int)ActualHeight);

    // 画可交互的矩形元素:点击时橙色,拖拽时深橙,平时蓝
    var elementBrush = _isElementClicked ? Brushes.Orange : (_isDragging ? Brushes.DarkOrange : Brushes.CornflowerBlue);
    g.FillRectangle(elementBrush, _elementPos.X, _elementPos.Y, 100, 60);
    g.DrawRectangle(Pens.Black, _elementPos.X, _elementPos.Y, 100, 60);

    // 画状态文本,显示当前交互状态
    using var font = new Font("Arial", 12);
    var statusText = _isDragging ? "拖拽中..." : (_isHovered ? "悬停中" : "就绪");
    g.DrawString(statusText, font, Brushes.Black, 10, 10);

    // 释放HDC
    ReleaseDC(hwnd, g.GetHdc());
}

// 导入Win32 API来获取和释放HDC
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);

[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

4. 在XAML里用这个控件

直接在页面的XAML里引用就行,记得加命名空间:

<Window
    x:Class="WinUI3GDIControlDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3GDIControlDemo"
    Title="GDI自定义控件演示">
    <Grid>
        <local:GDIDrawControl Width="400" Height="300"/>
    </Grid>
</Window>

要注意的点

  • 得先装System.Drawing.Common这个NuGet包,.NET 6及以上版本需要手动安装,不然用不了GDI+的API。
  • 如果需要拖拽时鼠标移出控件还能继续响应,记得在PointerPressed里调用CapturePointer(e.Pointer),PointerReleased里调用ReleasePointerCapture(e.Pointer)。
  • 只要状态变了,一定要调用InvalidateVisual(),不然控件不会重绘,UI就不会更新。
  • GDI是基于Win32的互操作,和WinUI 3本身的渲染机制不是一套,频繁重绘的场景可能会有性能问题,按需使用。

内容的提问来源于stack exchange,提问作者user3765739

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 04:02:13