← 全部文章 WinForm

WinForm 小坑合集

2022 年 1 月 19 日 · 2 分钟读完

目录
  1. 窗体的几个基本属性
  2. 图标
  3. 边框样式 FormBorderStyle
  4. 显示位置 StartPosition
  5. ComboBox 清空后的 SelectedIndex
  6. 窗体隐藏时不刷新
  7. 无边框窗体最大化遮住任务栏
  8. 只允许运行一个实例

几篇 WinForm 的短笔记,合成一篇方便查。

窗体的几个基本属性

图标

Icon 属性只支持 .ico 文件:

更换前后:

这只是窗体左上角和任务栏的图标。exe 文件本身的图标在 项目属性 → 应用程序 → 图标和清单 里设置。

边框样式 FormBorderStyle

值 含义 样式
Fixed3D 固定的三维边框
FixedDialog 固定的对话框粗边框
FixedSingle 固定的单线边框
FixedToolWindow 不可调大小的工具窗口
None 无边框
Sizable 可调大小(默认)
SizableToolWindow 可调大小的工具窗口

显示位置 StartPosition

常用的是 CenterScreen(屏幕居中)和 CenterParent(父窗体居中,对 ShowDialog 弹出的窗体有效)。设置成 Manual 时,由 Location 决定位置。

ComboBox 清空后的 SelectedIndex

Items 被清空后再设置选中项,只能设 SelectedIndex = -1,表示什么都不选;设成 0 会抛越界异常。

反过来也有个坑:DropDownStyle 是 DropDown(可输入)时,SelectedIndex = -1 有时清不掉文本框里的字,需要再设一次 comboBox1.Text = "";。

窗体隐藏时不刷新

工业项目里数据一直在变,变化时推送给界面更新。但切到别的页面、或者窗体被隐藏时,没必要继续刷新它,否则白白占用界面线程,影响正在显示的页面。

几种判断可见的方法:

bool v = form.Visible;            // 窗体本身是否可见

Form f = this.FindForm();         // 控件内部:找到所在的窗体
bool v2 = f != null && f.Visible;

bool v3 = this.Parent.Visible;    // 控件放在 Panel、GroupBox、TabPage 里时,看父容器

注意:Visible 只要自己和所有父级都可见才是 true。TabControl 里没被选中的 TabPage,它的 Visible 是 false,可以直接用来判断「这一页现在有没有被看着」。

更省事的做法是在更新入口统一判断:

void OnDataChanged(Data d)
{
    if (!this.Visible) { _dirty = true; return; } // 先记一下,不刷新
    Render(d);
}

protected override void OnVisibleChanged(EventArgs e)
{
    base.OnVisibleChanged(e);
    if (Visible && _dirty) { Render(_latest); _dirty = false; } // 重新可见时补一次
}

无边框窗体最大化遮住任务栏

FormBorderStyle = None 的窗体最大化时会盖住整个屏幕,包括任务栏。设置最大化时的范围为屏幕的工作区(不含任务栏):

this.MaximizedBounds = Screen.PrimaryScreen.WorkingArea;
// 或者限制最大尺寸
this.MaximumSize = Screen.PrimaryScreen.WorkingArea.Size;

多显示器时,用 Screen.FromControl(this).WorkingArea 取窗体所在的那块屏幕。工控机上需要真正全屏、盖住任务栏的,反而正好利用这个特性。

只允许运行一个实例

很多上位机软件不允许多开(会抢串口、抢相机)。原来的做法是按进程名查找:

static Process GetRunningInstance()
{
    Process current = Process.GetCurrentProcess();
    foreach (Process item in Process.GetProcessesByName(current.ProcessName))
    {
        if (item.Id != current.Id) return item;
    }
    return null;
}

[DllImport("User32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd, int cmdShow);
[DllImport("User32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);

static void Main()
{
    Process running = GetRunningInstance();
    if (running != null)
    {
        ShowWindowAsync(running.MainWindowHandle, 3); // 3 = SW_SHOWMAXIMIZED
        SetForegroundWindow(running.MainWindowHandle);
        return;
    }
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new FrmLoading());
}

按进程名判断有漏洞:exe 改个名字就能多开,同名的别的程序也会被误判。更可靠的是用命名互斥体:

static void Main()
{
    using (var mutex = new Mutex(true, @"Global\DP_MyApp_SingleInstance", out bool createdNew))
    {
        if (!createdNew)
        {
            MessageBox.Show("程序已经在运行。");
            return;
        }
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
}

Global\ 前缀让互斥体在所有用户会话间生效(远程桌面多用户时也只能开一个)。想同时把已有窗口调到前台,可以把上面的 ShowWindowAsync 和互斥体结合起来。

ShowWindowAsync 的第二个参数常用值(微软文档):

值 名称 效果
0 SW_HIDE 隐藏窗口
1 SW_SHOWNORMAL 激活并显示,最小化或最大化时还原
2 SW_SHOWMINIMIZED 激活并最小化
3 SW_SHOWMAXIMIZED 激活并最大化
5 SW_SHOW 激活,按当前大小和位置显示
6 SW_MINIMIZE 最小化,激活下一个顶层窗口
9 SW_RESTORE 激活并还原,还原最小化的窗口时用这个

把已经打开的程序调到前台,一般用 9(SW_RESTORE)比 3 更合适,不会改变用户原来的窗口大小。

# WinForm # 界面

原文首发于 blog.csdn.net