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

WinForms MonthCalendar二次打开卡顿问题求助

问题描述

我开发了一个基础WinForms弹窗,点击主窗体按钮即可弹出,仅包含MonthCalendar控件和确定/取消按钮。首次打开弹窗时,控件响应流畅,可正常切换月份、选择日期,日期变量更新及返回逻辑均正常;但点击确定关闭弹窗后再次打开,会出现严重卡顿:月份切换箭头无响应、鼠标悬停时箭头不高亮、鼠标点击选中的日期位置偏移(甚至无选中)等异常。


弹窗(CalendarForm)代码

public partial class CalendarForm : Form
{
    private DateTime selectedDate;
    private bool isDateSelected;

    public CalendarForm(DateTime initialDate)
    {
        InitializeComponent();
        InitializeCustomComponents(initialDate);
    }

    private void InitializeCustomComponents(DateTime initialDate)
    {
        this.FormBorderStyle = FormBorderStyle.FixedDialog;
        this.MaximizeBox = false;
        this.MinimizeBox = false;
        CultureInfo ci = new CultureInfo("es-ES");
        monthCalendar.TitleBackColor = System.Drawing.Color.LightBlue;
        monthCalendar.TitleForeColor = System.Drawing.Color.White;
        monthCalendar.TrailingForeColor = System.Drawing.Color.Gray;
        monthCalendar.FirstDayOfWeek = Day.Monday;
        monthCalendar.CalendarDimensions = new System.Drawing.Size(1, 1);
        monthCalendar.TodayDate = initialDate;
        monthCalendar.SelectionStart = initialDate;
        monthCalendar.SelectionEnd = initialDate;
        btnOK.Click += BtnOK_Click;
        btnCancel.Click += BtnCancel_Click;
    }

    private void BtnOK_Click(object sender, EventArgs e)
    {
        selectedDate = monthCalendar.SelectionStart;
        isDateSelected = true;
        this.DialogResult = DialogResult.OK;
    }

    private void BtnCancel_Click(object sender, EventArgs e)
    {
        isDateSelected = false;
        this.DialogResult = DialogResult.Cancel;
    }

    public (DateTime, bool) GetSelectedDate()
    {
        return (selectedDate, isDateSelected);
    }
}

弹窗调用逻辑

private async void btnCalendar_Click(object sender, EventArgs e)
{
    bool updated = false;
    using (CalendarForm calendarForm = new CalendarForm(selectedDate))
    {
        if (calendarForm.ShowDialog() == DialogResult.OK)
        {
            (DateTime date, bool isDateSelected) = calendarForm.GetSelectedDate();
            if (isDateSelected)
            {
                selectedDate = date;
                updated = true;
            }
        }
    }
    if (updated)
    {
        await LoadDataForDate(selectedDate);
    }
    
}

补充:LoadDataForDate 代码

private async Task LoadDataForDate(DateTime date)
{
    string dateString = date.ToString("yyyyMMdd");
    basketLayers = await Task.Run(() => Backend.GetHeatData(dateString, globalversion, 0));

    DataTable heatBasketTable = CreateHeatBasketDataTable(basketLayers);
    dataGridViewHeatBaskets.DataSource = heatBasketTable;

    DataTable basketLayerTable = CreateBasketLayerDataTable(basketLayers);
    dataGridViewBasketLayers.DataSource = basketLayerTable;

    SetColumnHeaders(1); 

    lblSelectedDate.Text = date.ToString("yyyy-MM-dd");

    MakeHeadersNonClickable(dataGridViewBasketLayers);
    MakeHeadersNonClickable(dataGridViewHeatBaskets);

    dataGridViewBasketLayers.DataBindingComplete += DataGridViewBasketLayers_DataBindingComplete;
}

补充:GetHeatData 代码(含伪代码)

public static async Task<List<BasketLayer>> GetHeatData(string date)
{
    List<BasketLayer> basketLayers = new List<BasketLayer>();

    using (SqlConnection localcnn = new SqlConnection(_connetionString))
    {
        await localcnn.OpenAsync();
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("some stuff, using the date argument");
        SqlCommand comando = new SqlCommand(sb.ToString(), localcnn);
        SqlDataReader reader = await comando.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            int someVariables = int.Parse(reader.GetString(reader.GetOrdinal("SQLvariables")));

            BasketLayer basketLayer = new BasketLayer(someFields);
            basketLayers.Add(basketLayer);
        }
        reader.Close();
    }

    Dictionary<int, int> basketOffsetDict = new Dictionary<int, int>();
    foreach (var basketLayer in basketLayers)
    {
        //a small routine to update a class field
        basketOffset = CheckForOlderBaskets(somevariables); //this should be async because it's an SQL I/O 
        basketLayer.Basket += basketOffset;
    }
    return basketLayers;
}

补充:DataBindingComplete 事件取消订阅代码

private void DataGridViewBasketLayers_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
    ReadOnlyData();
    dataGridViewBasketLayers.DataBindingComplete -= DataGridViewBasketLayers_DataBindingComplete;
}

补充:ReadOnlyData 代码

private void ReadOnlyData()
{
    foreach (DataGridViewColumn column in dataGridViewHeatBaskets.Columns)
    {
        column.ReadOnly = true;
    }

    if (globalversion == "original")
    {
        foreach (DataGridViewColumn column in dataGridViewBasketLayers.Columns)
        {
            column.ReadOnly = true;
        }
    }
    else if (globalversion == "update")
    {
        dataGridViewBasketLayers.Columns["Col1"].ReadOnly = true;
        dataGridViewBasketLayers.Columns["Col2"].ReadOnly = true;
        dataGridViewBasketLayers.Columns["Col3"].ReadOnly = true;
        dataGridViewBasketLayers.Columns["Col4"].ReadOnly = true;

        foreach (DataGridViewRow row in dataGridViewBasketLayers.Rows)
        {
            if (!RowHasValues(row))
            {
                row.Cells["Col5"].ReadOnly = true;
            }
            else
            {
                row.Cells["Col5"].ReadOnly = false;
            }
        }
    }
}

问题分析与解决方案

核心问题:UI线程资源占用异常

二次打开弹窗卡顿的根源是UI线程消息循环被干扰,或线程池资源被长期占用,具体修复点如下:

  1. DataGridView事件重复绑定隐患
    每次调用LoadDataForDate都会绑定DataBindingComplete事件,虽然事件处理中会取消订阅,但如果某次绑定后事件未触发,订阅会累积,后续UI操作时多个回调抢占资源,间接影响弹窗控件响应。
    修复:绑定前先移除旧订阅,确保每次只有一个订阅存在:

    // 先取消旧订阅,再绑定新的
    dataGridViewBasketLayers.DataBindingComplete -= DataGridViewBasketLayers_DataBindingComplete;
    dataGridViewBasketLayers.DataBindingComplete += DataGridViewBasketLayers_DataBindingComplete;
    
  2. 同步SQL操作阻塞线程池
    CheckForOlderBaskets是同步SQL操作,放在Task.Run中会长时间占用线程池线程,导致WinForms控件的消息循环处理延迟(MonthCalendar对UI消息敏感度高)。
    修复:将CheckForOlderBaskets改造为异步方法,用await调用:

    // 假设改造后的异步方法名为CheckForOlderBasketsAsync
    basketOffset = await CheckForOlderBasketsAsync(somevariables);
    basketLayer.Basket += basketOffset;
    
  3. MonthCalendar文化信息未应用
    代码中创建了CultureInfo但未绑定到控件,可能导致控件内部渲染计算异常,二次打开时触发卡顿。
    修复:将文化信息应用到控件:

    // 在InitializeCustomComponents方法中添加
    monthCalendar.Culture = ci;
    
  4. 弹窗关闭时事件未清理
    虽然用using块释放弹窗,但控件绑定的事件可能未及时清理,残留的引用影响资源回收。
    修复:在CalendarForm中添加关闭时的事件清理:

    // 在CalendarForm类中添加
    private void CalendarForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        btnOK.Click -= BtnOK_Click;
        btnCancel.Click -= BtnCancel_Click;
    }
    // 在InitializeCustomComponents中绑定该事件
    this.FormClosing += CalendarForm_FormClosing;
    

验证步骤

  1. 先修复事件重复绑定问题,测试二次打开弹窗是否恢复正常;
  2. 若仍卡顿,再处理CheckForOlderBaskets的异步改造;
  3. 最后添加文化信息绑定和弹窗事件清理,保障长期运行稳定性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 23:50:56