当单击发生在子项之外时调用 ListViews base.WndProc 时的 ArgOutOfRangeEx,C# 10.0

发布于 2025-01-12 08:02:49 字数 5872 浏览 0 评论 0原文

我在 OwnerDrawn 列表视图中调用 base.WndProc 时收到 ArgumentOutOfRangeException。

在任何 ListViewItem 的最后一个子项的右侧(空白区域)执行单击后,会发生异常。

列表视图列宽度以编程方式设置为填充整个列表视图,但是有时未按预期接收数据,导致一些额外的空间,这是通常不会发生的边缘情况。

异常时要处理的消息是WM_LBUTTONDOWN(0x0201)或WM_RBUTTONDOWN(0x0204)。

用于绘制 LV 子项的所有数据都来自 LVI 标签引用的类,我在任何时候都不会尝试读取子项,也不会将数据写入任何子项。 下面是最小的可重现代码,采用 C# (10.0),使用 .NET 6.0 显示异常。

我试图简单地排除 WM_LBUTTONDOWN 的处理。这确实消除了异常,但它也阻止了左键单击事件到达所需的 MouseUp。 (也适用于右键单击)

虽然我正在努力修复收到错误数据或没有数据后列大小调整中的错误,但我想检查是否存在此可能的异常,并在异常发生之前从该方法返回。

数据类

namespace testCase;
class myClass
{
    public string s1 = "subitem1";
    public string s2 = "subitem2";
}

表单代码

namespace testCase;
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        ListViewExWatch lv = new();
        Controls.Add(lv);
        lv.MouseUp += Lv_MouseUp;
        lv.Top = 0;
        lv.Left = 0;
        lv.Width = ClientSize.Width;
        lv.Height = ClientSize.Height;
        lv.OwnerDraw = true;
        lv.BackColor = Color.AntiqueWhite;
        lv.Columns.Add("Row", 50);
        lv.Columns.Add("sub1", 50);
        lv.Columns.Add("sub2", 50);

        for(int i = 0; i < 10; i++)
        {
            ListViewItem lvi = new(){ Text = "Row " + i, Tag = new myClass() };
            lvi.SubItems.Add("");
            lvi.SubItems.Add("");
            lv.Items.Add(lvi);
        }
                
    }

    private void Lv_MouseUp(object? sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            MessageBox.Show("Left click - doing action A");
        }
        if (e.Button == MouseButtons.Right)
        {
            MessageBox.Show("Right click - Creating context menu");
        }
    }
}

自定义 ListView 覆盖

using System.Runtime.InteropServices;
namespace testCase;
public class ListViewExWatch : ListView
{
    #region Windows API
    [StructLayout(LayoutKind.Sequential)]
    struct DRAWITEMSTRUCT
    {
        public int    ctlType;
        public int    ctlID;
        public int    itemID;
        public int    itemAction;
        public int    itemState;
        public IntPtr hWndItem;
        public IntPtr hDC;
        public int    rcLeft;
        public int    rcTop;
        public int    rcRight;
        public int    rcBottom;
        public IntPtr itemData;
    }

    const int LVS_OWNERDRAWFIXED = 0x0400;
    const int WM_SHOWWINDOW      = 0x0018;
    const int WM_DRAWITEM        = 0x002B;
    const int WM_MEASUREITEM     = 0x002C;
    const int WM_REFLECT         = 0x2000;
    const int WM_LBUTTONDOWN     = 0x0201;
    #endregion

    public ListViewExWatch()
    {
        SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams k_Params = base.CreateParams;
            k_Params.Style |= LVS_OWNERDRAWFIXED;
            return k_Params;
        }
    }


    protected override void WndProc(ref Message k_Msg)
    {
        //if (k_Msg.Msg == WM_LBUTTONDOWN) return;
        base.WndProc(ref k_Msg); // Exception: System.ArgumentOutOfRangeException: 'InvalidArgument=Value of '-1' is not valid for 'index'. 
        // Only occurs when clicking to the right of the last subItem

        switch (k_Msg.Msg)
        {
            case WM_SHOWWINDOW:
                View = View.Details;
                OwnerDraw = false;
                break;
            case WM_REFLECT + WM_MEASUREITEM:
                Marshal.WriteInt32(k_Msg.LParam + 4 * sizeof(int), 14);
                k_Msg.Result = (IntPtr)1;
                break;
            case WM_REFLECT + WM_DRAWITEM:
                {
                    object? lParam = k_Msg.GetLParam(typeof(DRAWITEMSTRUCT));
                    if (lParam is null) throw new Exception("lParam shouldn't be null");
                    DRAWITEMSTRUCT k_Draw = (DRAWITEMSTRUCT)lParam;
                    using Graphics gfx = Graphics.FromHdc(k_Draw.hDC);

                    ListViewItem lvi = Items[k_Draw.itemID];
                    myClass wi = (myClass)lvi.Tag;

                    TextRenderer.DrawText(gfx, lvi.Text, Font, lvi.SubItems[0].Bounds, Color.Black, TextFormatFlags.Left);
                    TextRenderer.DrawText(gfx, wi.s1, Font, lvi.SubItems[1].Bounds, Color.Black, TextFormatFlags.Left);
                    TextRenderer.DrawText(gfx, wi.s2, Font, lvi.SubItems[2].Bounds, Color.Black, TextFormatFlags.Left);
                    break;
                }
        }
    }
}

错误消息

System.ArgumentOutOfRangeException
  HResult=0x80131502
  Message=InvalidArgument=Value of '-1' is not valid for 'index'. Arg_ParamName_Name
ArgumentOutOfRange_ActualValue
  Source=System.Windows.Forms
  StackTrace:
   at System.Windows.Forms.ListViewItem.ListViewSubItemCollection.get_Item(Int32 index)
   at System.Windows.Forms.ListView.HitTest(Int32 x, Int32 y)
   at System.Windows.Forms.ListView.ListViewAccessibleObject.HitTest(Int32 x, Int32 y)
   at System.Windows.Forms.ListView.WmMouseDown(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.ListView.WndProc(Message& m)
   at testCase.ListViewExWatch.WndProc(Message& k_Msg) in C:\Users\XXXX\source\repos\testCase\ListViewExWatch.cs:line 50
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, WM msg, IntPtr wparam, IntPtr lparam)

从错误中可以明显看出,基本列表视图代码试图获取不存在的子项,因为单击不在子项上,因此 -1 表示索引内错误信息。

发生异常时,k_Msg 中不存在包含索引 (-1) 的成员,因此我无法检查并简单地返回。

我可以将对 base.WndProc 的调用包含在 try catch 中,因为它是一种边缘情况。我一直有这样的心态:检查可能的异常并尽可能阻止它们,而不是捕获它们。但这一件却把我难住了。 我在这方面是不是太迂腐了?

我很可能在这里遗漏了一些基本的东西,各位好心人能否指出我正确的方向?

I am receiving a ArgumentOutOfRangeException while calling base.WndProc in an OwnerDrawn listview.

The exception occurs after a click is performed to the right (empty space) of the last subitem of any ListViewItem.

The listview column widths are programmatically set to fill the entire listview, however on occasion data is not received as expected leading to some extra space, this is an edge case which usually does not occur.

The message to be processed at the time of exception is WM_LBUTTONDOWN (0x0201), or WM_RBUTTONDOWN (0x0204).

All data for painting the LV subitems is from a class referenced by the tag of the LVI, at no point do I try to read the subitems, nor do I write data to any of the subitems.
Below is the smallest reproducible code, in C# (10.0), using .NET 6.0 that shows the exception.

I attempted to simply exclude processing of WM_LBUTTONDOWN. This did remove the exception, however it also stopped left click events from reaching MouseUp which is required. (also for right clicks)

Whilst I am working to fix my errors in column sizing after receiving bad or no data, I would like to check for this possible exception and simply return from the method before the exception can occur.

Data class

namespace testCase;
class myClass
{
    public string s1 = "subitem1";
    public string s2 = "subitem2";
}

Form code

namespace testCase;
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        ListViewExWatch lv = new();
        Controls.Add(lv);
        lv.MouseUp += Lv_MouseUp;
        lv.Top = 0;
        lv.Left = 0;
        lv.Width = ClientSize.Width;
        lv.Height = ClientSize.Height;
        lv.OwnerDraw = true;
        lv.BackColor = Color.AntiqueWhite;
        lv.Columns.Add("Row", 50);
        lv.Columns.Add("sub1", 50);
        lv.Columns.Add("sub2", 50);

        for(int i = 0; i < 10; i++)
        {
            ListViewItem lvi = new(){ Text = "Row " + i, Tag = new myClass() };
            lvi.SubItems.Add("");
            lvi.SubItems.Add("");
            lv.Items.Add(lvi);
        }
                
    }

    private void Lv_MouseUp(object? sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            MessageBox.Show("Left click - doing action A");
        }
        if (e.Button == MouseButtons.Right)
        {
            MessageBox.Show("Right click - Creating context menu");
        }
    }
}

Custom ListView override

using System.Runtime.InteropServices;
namespace testCase;
public class ListViewExWatch : ListView
{
    #region Windows API
    [StructLayout(LayoutKind.Sequential)]
    struct DRAWITEMSTRUCT
    {
        public int    ctlType;
        public int    ctlID;
        public int    itemID;
        public int    itemAction;
        public int    itemState;
        public IntPtr hWndItem;
        public IntPtr hDC;
        public int    rcLeft;
        public int    rcTop;
        public int    rcRight;
        public int    rcBottom;
        public IntPtr itemData;
    }

    const int LVS_OWNERDRAWFIXED = 0x0400;
    const int WM_SHOWWINDOW      = 0x0018;
    const int WM_DRAWITEM        = 0x002B;
    const int WM_MEASUREITEM     = 0x002C;
    const int WM_REFLECT         = 0x2000;
    const int WM_LBUTTONDOWN     = 0x0201;
    #endregion

    public ListViewExWatch()
    {
        SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams k_Params = base.CreateParams;
            k_Params.Style |= LVS_OWNERDRAWFIXED;
            return k_Params;
        }
    }


    protected override void WndProc(ref Message k_Msg)
    {
        //if (k_Msg.Msg == WM_LBUTTONDOWN) return;
        base.WndProc(ref k_Msg); // Exception: System.ArgumentOutOfRangeException: 'InvalidArgument=Value of '-1' is not valid for 'index'. 
        // Only occurs when clicking to the right of the last subItem

        switch (k_Msg.Msg)
        {
            case WM_SHOWWINDOW:
                View = View.Details;
                OwnerDraw = false;
                break;
            case WM_REFLECT + WM_MEASUREITEM:
                Marshal.WriteInt32(k_Msg.LParam + 4 * sizeof(int), 14);
                k_Msg.Result = (IntPtr)1;
                break;
            case WM_REFLECT + WM_DRAWITEM:
                {
                    object? lParam = k_Msg.GetLParam(typeof(DRAWITEMSTRUCT));
                    if (lParam is null) throw new Exception("lParam shouldn't be null");
                    DRAWITEMSTRUCT k_Draw = (DRAWITEMSTRUCT)lParam;
                    using Graphics gfx = Graphics.FromHdc(k_Draw.hDC);

                    ListViewItem lvi = Items[k_Draw.itemID];
                    myClass wi = (myClass)lvi.Tag;

                    TextRenderer.DrawText(gfx, lvi.Text, Font, lvi.SubItems[0].Bounds, Color.Black, TextFormatFlags.Left);
                    TextRenderer.DrawText(gfx, wi.s1, Font, lvi.SubItems[1].Bounds, Color.Black, TextFormatFlags.Left);
                    TextRenderer.DrawText(gfx, wi.s2, Font, lvi.SubItems[2].Bounds, Color.Black, TextFormatFlags.Left);
                    break;
                }
        }
    }
}

The error message

System.ArgumentOutOfRangeException
  HResult=0x80131502
  Message=InvalidArgument=Value of '-1' is not valid for 'index'. Arg_ParamName_Name
ArgumentOutOfRange_ActualValue
  Source=System.Windows.Forms
  StackTrace:
   at System.Windows.Forms.ListViewItem.ListViewSubItemCollection.get_Item(Int32 index)
   at System.Windows.Forms.ListView.HitTest(Int32 x, Int32 y)
   at System.Windows.Forms.ListView.ListViewAccessibleObject.HitTest(Int32 x, Int32 y)
   at System.Windows.Forms.ListView.WmMouseDown(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.ListView.WndProc(Message& m)
   at testCase.ListViewExWatch.WndProc(Message& k_Msg) in C:\Users\XXXX\source\repos\testCase\ListViewExWatch.cs:line 50
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, WM msg, IntPtr wparam, IntPtr lparam)

From the error it is apparent that the base listview code it is trying to get a subitem that doesn't exist because the click is not on a subitem, hence the -1 for index within the error message.

At the time of exception there is no member of k_Msg that contains the index (-1), so I cannot check for that and simply return.

I could surround the call to base.WndProc in a try catch, since it is an edge case. I've always had the mindset to check for possible exceptions and prevent them whenever possible rather than catch them. But this one has me stumped.
Am I being too pedantic in this regard?

Most likely I am missing something basic here, could you fine folks please point me in the right direction?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

仅冇旳回忆 2025-01-19 08:02:49

我已经实现了以下代码来仅捕获这个特定的异常,并且仍然允许任何其他异常根据需要冒泡。

try
{
    base.WndProc(ref k_Msg); // This throws a ArgOutOfRangeEx when a click is performed to the right of any subitem (empty space)
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "index" && (int?)ex.ActualValue == -1 && ex.TargetSite?.DeclaringType?.Name == "ListViewSubItemCollection")
{
    Program.Log(LogLevel.Normal, "ListViewExWatch.WndProc()", "ArgumentOutOfRangeException: A click has been perfored outside of a valid subitem. This has been handled and indicates column witdth calcs were wrong.");
    return;
}

然而,通过进一步的研究,我设法专门解决了这个问题

https ://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown

l参数

低位字指定光标的 x 坐标...
高位字指定光标的 y 坐标...

我最终在处理消息之前修改了lParam。通过将 x 坐标移动到最后一个子项目内部,可以避免异常,并且可以正常处理点击。

protected override void WndProc(ref Message k_Msg)
{
    if (k_Msg.Msg == WM_LBUTTONDOWN || k_Msg.Msg == WM_RBUTTONDOWN)
    {
        // Get the x position of the end of the last subitem
        int width = -1;
        foreach (ColumnHeader col in Columns) width += col.Width;

        // Where did the click occur?
        int x_click = SignedLOWord(k_Msg.LParam);
        int y_click = SignedHIWord(k_Msg.LParam);

        // If the click is to the right of the last subitem, set the x-coordinate to inside the last subitem.
        if (x_click > width) k_Msg.LParam = MakeLparam(width, y_click);
    }

    base.WndProc(ref k_Msg);
    ...

有趣的是,Lv_MouseUp 处理程序中仍然报告了正确的 x 和 y 坐标。对于其他处理程序(例如 MouseDown ),情况可能并非如此,它显然已被修改,因为我不使用它们,所以没有出现问题

这里是上面使用的辅助函数(来自 referencesource.microsoft.com)

public static IntPtr MakeLparam(int low, int high)
{
    return (IntPtr)((high << 16) | (low & 0xffff));
}
public static int SignedHIWord(IntPtr n)
{
    return SignedHIWord(unchecked((int)(long)n));
}
public static int SignedLOWord(IntPtr n)
{
    return SignedLOWord(unchecked((int)(long)n));
}
public static int SignedHIWord(int n)
{
    return (short)((n >> 16) & 0xffff);
}
public static int SignedLOWord(int n)
{
    return (short)(n & 0xFFFF);
}

I had implemented the following code to catch only this specific exception and still allow any other to bubble up as desired.

try
{
    base.WndProc(ref k_Msg); // This throws a ArgOutOfRangeEx when a click is performed to the right of any subitem (empty space)
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "index" && (int?)ex.ActualValue == -1 && ex.TargetSite?.DeclaringType?.Name == "ListViewSubItemCollection")
{
    Program.Log(LogLevel.Normal, "ListViewExWatch.WndProc()", "ArgumentOutOfRangeException: A click has been perfored outside of a valid subitem. This has been handled and indicates column witdth calcs were wrong.");
    return;
}

With further research however, I managed to specifically workaround the issue

From https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown

lParam

The low-order word specifies the x-coordinate of the cursor...
The high-order word specifies the y-coordinate of the cursor...

I ended up modifying the lParam before processing the message. By moving the x-coordinate to inside the last subitem the exception is averted and clicks are processed as normal.

protected override void WndProc(ref Message k_Msg)
{
    if (k_Msg.Msg == WM_LBUTTONDOWN || k_Msg.Msg == WM_RBUTTONDOWN)
    {
        // Get the x position of the end of the last subitem
        int width = -1;
        foreach (ColumnHeader col in Columns) width += col.Width;

        // Where did the click occur?
        int x_click = SignedLOWord(k_Msg.LParam);
        int y_click = SignedHIWord(k_Msg.LParam);

        // If the click is to the right of the last subitem, set the x-coordinate to inside the last subitem.
        if (x_click > width) k_Msg.LParam = MakeLparam(width, y_click);
    }

    base.WndProc(ref k_Msg);
    ...

It is interesting to note the correct x and y coordinates are still reported in the Lv_MouseUp handler. This may not be the case for other handlers such as MouseDown which has obviously been modified, since I do not use them it has not presented a problem

Here are the helper functions used above (from referencesource.microsoft.com)

public static IntPtr MakeLparam(int low, int high)
{
    return (IntPtr)((high << 16) | (low & 0xffff));
}
public static int SignedHIWord(IntPtr n)
{
    return SignedHIWord(unchecked((int)(long)n));
}
public static int SignedLOWord(IntPtr n)
{
    return SignedLOWord(unchecked((int)(long)n));
}
public static int SignedHIWord(int n)
{
    return (short)((n >> 16) & 0xffff);
}
public static int SignedLOWord(int n)
{
    return (short)(n & 0xFFFF);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文