我需要一些帮助在 WinForms 中创建一个搜索框(类似于 Windows 11 任务管理器中的搜索框;见下图)。

它应该支持在开始时显示一个图标,并在结束时显示一个“清除”按钮。

我曾尝试使用合成技术自己创建一个,但遇到的问题是搜索按钮位于文本框的顶部,无法在文本框下方输入文本,太丑了。

如果我使用具有“窗口”背景颜色的面板进行合成,则可以处理整个布局问题,但是当文本框获得焦点时,蓝色焦点栏不会在整个控件上呈现,从而使其在具有多个文本框的表单上看起来不合适。

有人能帮我解决其中任何一个问题吗?

6

  • 3
    如果不进行大量自定义绘制,在 WinForms 中,这样的事情实际上不可行。我建议您切换到 WPF/WinUI。


    – 

  • 1
    @Dai 是正确的,您可以在 WinForms 中执行此操作,但您必须进行大量第三方已为您完成的低级编辑。基于 Xaml 的框架对此具有更具声明性的过程,因此如果您想管理 WinForms 中 OOTB 之外的外观和感觉,并且不想购买已完成此操作的第三方库,它们将更合适。


    – 

  • 图标可以在 中呈现OnPaint,按钮可以通过Controls.Add方法简单添加。要让它们有足够的空间,只需窗口消息EM_SETMARGINS。对于提示文本,只需使用PlaceholderText属性(自 .NET Core 3.0 起可用)


    – 

  • @shingo 这是 Windows11 标准主题吗?


    – 

  • shingo & Ralf – 这是默认的 Win 11 主题。这是 Windows 11 上任务管理器的搜索框。


    – 


最佳答案
2

自定义 TextBox 控件的示例,其应显示与问题中描述的控件类似(或多或少)的行为。

  • 此控件捕获以重新定义非客户端区域。组合区域描述了专用于显示图像的非客户端左侧区域和允许绘制轮廓的底部区域,
    WM_NCCALCSIZE当此控件公开的公共属性之一(用于重新定义轮廓的颜色和大小)在运行时或设计时设置时,也会触发该区域
  • 当收到消息时,图像对象和轮廓就会在非客户区中绘制。
  • 向底层 Win32 控件发送EM_SETCUEBANNER 消息即可激活水印提示横幅)。WParam设置为非零,允许在控件获得焦点时显示提示横幅。输入一些文本或双击控件时,将删除提示横幅
  • 有一个对 的调用SetWindowTheme(),只是因为我更喜欢这种方式。根据需要进行修改(即,您可以删除该调用,这不是严格要求的;看看您是否喜欢这样。可能需要对渲染过程进行一些更改)

这是它在运行时的工作方式。同样也适用于设计时,我没有添加任何阻止它的东西:


using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;

[ToolboxItem(true), DesignerCategory("code")]
public class TextBoxOutline : TextBox {
    int outlineHeight = 1;
    Color outlineColor = SystemColors.Highlight;
    Image? lensImage = null;
    int imagePadding = 0;

    public TextBoxOutline() {
        SetStyle(ControlStyles.ResizeRedraw, true);
        lensImage = [Some Image];
        imagePadding = Height + 4;
    }

    [DefaultValue(1)]
    public int OutlineHeight { 
        get => outlineHeight;
        set {
            if (value != outlineHeight) {
                outlineHeight = Math.Max(Math.Min(value, 4), 1);
                InvokeNcCalcSize();
            }
        } 
    }

    [DefaultValue(typeof(Color), "Highlight")]
    public Color OutlineColor { 
        get => outlineColor;
        set {
            if (value != outlineColor) { 
                outlineColor = value;
                Invalidate();
                InvokeNcCalcSize();
            }
        }
    }

    protected override void OnHandleCreated(EventArgs e) {
        SetWindowTheme(Handle, "", "");
        base.OnHandleCreated(e);
        SendMessage(Handle, EM_SETCUEBANNER, 1, "Placeholder Text");
    }

    protected override void WndProc(ref Message m) {
        switch (m.Msg) {
            case WM_NCCALCSIZE:
                WmNcCalcSize(ref m);
                break;
            case WM_NCPAINT:
                WmNcPaint(ref m);
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }

    protected virtual void WmNcCalcSize(ref Message m) {
        if (nint.Zero == m.WParam) {
            var client = Marshal.PtrToStructure<RECT>(m.LParam);
            client.Left += imagePadding;
            client.Bottom -= outlineHeight;
            Marshal.StructureToPtr(client, m.LParam, true);
            m.Result = nint.Zero;
        }
        else {
            var calcParams = Marshal.PtrToStructure<NCCALCSIZE_PARAMS>(m.LParam);
            calcParams.rgrc[0].Left += imagePadding;
            calcParams.rgrc[0].Bottom -= outlineHeight;
            Marshal.StructureToPtr(calcParams, m.LParam, true);
            m.Result = 0x0010 | 0x0020 | 0x0300;
        }
    }

    protected virtual void WmNcPaint(ref Message m) {
        nint hDC = nint.Zero;
        bool deleteDC = false;
        Rectangle clipRegion = Rectangle.Empty;

        if (1 == m.WParam) {
            deleteDC = true;
            hDC = GetWindowDC(m.HWnd);
            clipRegion = new Rectangle(imagePadding, 0, Width - imagePadding, Height - outlineHeight);
        }
        else {
            hDC = GetDCEx(Handle, m.WParam, DCX_WINDOW | DCX_USESTYLE);
        }

        if (hDC != nint.Zero) {
            using var g = Graphics.FromHdc(hDC);
            using var pen = new Pen(outlineColor, outlineHeight);
            g.Clear(BackColor);
            g.SmoothingMode = SmoothingMode.AntiAlias;
            g.DrawImage(lensImage!, 0, 0, ClientSize.Height - 4, ClientSize.Height - 4);

            if (clipRegion != Rectangle.Empty) {
                g.ExcludeClip(clipRegion);
            }

            g.DrawLine(pen, 0,
                g.VisibleClipBounds.Bottom - 1,
                g.VisibleClipBounds.Width,
                g.VisibleClipBounds.Bottom - 1
            );

            if (deleteDC) ReleaseDC(Handle, hDC);
        }
        m.Result = nint.Zero;
    }

    private void InvokeNcCalcSize() {
        SetWindowPos(Handle, nint.Zero, 0, 0, 0, 0, 
            SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOSENDCHANGING);
    }

    const int EM_SETCUEBANNER = 0x1501;
    const int WM_NCCALCSIZE = 0x0083;
    const int WM_NCPAINT = 0x0085;
    const uint DCX_WINDOW = 0x00000001;
    const uint DCX_EXCLUDERGN = 0x00000040;
    const uint DCX_INTERSECTRGN = 0x00000080;
    const uint DCX_USESTYLE = 0x00010000;
    const uint SWP_NOSIZE = 0x0001;
    const uint SWP_NOMOVE = 0x0002;
    const uint SWP_NOZORDER = 0x0004;
    const uint SWP_FRAMECHANGED = 0x0020;
    const uint SWP_SHOWWINDOW = 0x0040;
    const uint SWP_NOSENDCHANGING = 0x0400;

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    internal static extern int SendMessage(nint hWnd, int msg, int wParam, string lParam);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

    [DllImport("UxTheme.dll", SetLastError = true, CharSet = CharSet.Auto)]
    internal static extern nint SetWindowTheme(nint hwnd, string pszSubAppName, string pszSubIdList);

    [DllImport("user32.dll")]
    internal static extern nint GetDCEx(nint hWnd, nint hrgnClip, uint flags);

    [DllImport("user32.dll")]
    internal static extern nint GetWindowDC(nint hWnd);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern bool ReleaseDC(nint hWnd, nint hDc);

    [StructLayout(LayoutKind.Sequential)]
    internal struct NCCALCSIZE_PARAMS {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
        public RECT[] rgrc;
        public WINDOWPOS lppos;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct WINDOWPOS {
        public nint hwnd;
        public nint hwndInsertAfter;
        public int x;
        public int y;
        public int cx;
        public int cy;
        public uint flags;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct RECT {
        public int Left, Top, Right, Bottom;

        public RECT(int left, int top, int right, int bottom) {
            Left = left; Top = top; Right = right; Bottom = bottom;
        }
        public static RECT FromRectangle(Rectangle r) => new(r.Left, r.Top, r.Bottom, r.Right);

        public Rectangle ToRectangle() => Rectangle.FromLTRB(Left, Top, Right, Bottom);
        public Size Size => new(Right - Left, Bottom - Top);
    }
}

4

  • 哇 @Jimi!这需要做很多工作,我真的很感激。不幸的是,它并没有解决问题。我知道我没有直接在问题中说出来,但它暗示了:我需要一个使用操作系统主题的文本框控件。蓝色焦点栏在 Win10 上不存在 – 谁知道 Win12 会带来什么。我正在尝试创建一个控件,该控件具有与操作系统相同的视觉效果,因此主题与表单上的其他文本框相同。再次感谢,我感谢您的努力。


    – 

  • 我以为你问的是复制这种风格的控件。如果你想保留系统主题,那就简单多了:你可以看到,在这个控件中,我在非客户区创建了两个部分,一个用于绘制轮廓,一个用于绘制位图(但你可以使用 ControlPaint 绘制不同状态下的按钮)。所以,你只需要保留左侧的区域;或者,用同样的方法,在右侧生成另一个区域,你可以在其中放置按钮或其他任何东西。轮廓的存在仅基于系统主题


    – 


  • 我在 NC 绘画中遇到了一些问题……但它很有前途。谢谢你给我指明了方向。


    – 

  • 如果这不是您想要的答案,请不要觉得有义务接受答案。顺便说一句,当我有时间时,我将发布一个修改后的实现——NC 绘画最初可能不友好,主要是因为在 WinForms 中,您习惯于接收已经考虑剪辑区域的 PaintEventArgs 对象,您可以忽略该部分。然后是 WParam 的事情:当它非零时,GetDcEx()始终返回一个空区域;当它为零时,系统希望您验证该区域,您必须使用未记录的标志调用它,DCX_USESTYLEDCX_EXCLUDERGN在需要时等。


    – 

您可以通过重写 OnPaint 方法来定制文本框的绘制方式。