How do I know when the WinForms ListView scrollbar reaches it's bottom?
When this happens, I want the listview to be populated with more data (which is endless in theory in my case).
The OnScroll event gives me the scroll value from the top, but I have no way of knowing if the user can scroll any further or not.
I found an answer using some code from the great ObjectListView code-project:
http://www.codeproject.com/KB/list/ObjectListView.aspx
call GetScrollInfo:
private const int SIF_RANGE = 0x0001;
private const int SIF_PAGE = 0x0002;
private const int SIF_POS = 0x0004;
private const int SIF_DISABLENOSCROLL = 0x0008;
private const int SIF_TRACKPOS = 0x0010;
private const int SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS);
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool GetScrollInfo(IntPtr hWnd, int fnBar, SCROLLINFO scrollInfo);
public static SCROLLINFO GetFullScrollInfo(ListView lv, bool horizontalBar) {
int fnBar = (horizontalBar ? SB_HORZ : SB_VERT);
SCROLLINFO scrollInfo = new SCROLLINFO();
scrollInfo.fMask = SIF_ALL;
if (GetScrollInfo(lv.Handle, fnBar, scrollInfo))
return scrollInfo;
else
return null;
}
with this data struct:
[StructLayout(LayoutKind.Sequential)]
public class SCROLLINFO
{
public int cbSize = Marshal.SizeOf(typeof(SCROLLINFO));
public int fMask;
public int nMin;
public int nMax;
public int nPage;
public int nPos;
public int nTrackPos;
}
the nMax gives the total max scroll value including the scroll handle itself, so the actually useful max value is nMax - nPage, where nPage is the size of the scroll handle.
This works great !
I'm not able to answer your question directly, but from your description, it sounds like you really want to look into using the virtual mode of a list view for managing a large dataset.
http://msdn.microsoft.com/en-us/library/system.windows.forms.listview.virtualmode.aspx
In the case somebody needs this, I borrow the above code and I improved a little bit to handle onMaximumBottomScroll event using keyboard with down, next page, and end keys, dragging or clicking the scrollbar and reaching the max bottom, and using the mousewheel on vertically or horizontally. This is working for me like a charm.
public partial class OrganizationFilesListView : ListView
{
// Windows messages
private const int WM_VSCROLL = 0x0115;
private const int WM_MOUSEHWHEEL = 0x020E;
private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_KEYDOWN = 0x0100;
// ScrollBar types
private const int SB_VERT = 1; // only for maximum vertical scroll position
// ScrollBar interfaces
private const int SIF_TRACKPOS = 0x10;
private const int SIF_RANGE = 0x01;
private const int SIF_POS = 0x04;
private const int SIF_PAGE = 0x02;
private const int SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS;
// variable to force to run only once the event
private bool runningOnMaximumBottomScroll = false;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
SCROLLINFO si = new SCROLLINFO();
si.cbSize = (uint)Marshal.SizeOf(si);
si.fMask = (uint)ScrollInfoMask.SIF_ALL;
bool isMaximumButtomScroll = false;
switch (m.Msg)
{
case WM_VSCROLL:
isMaximumButtomScroll = GetScrollInfo(m.HWnd, SB_VERT, ref si) ? (si.nPage + si.nPos) >= si.nMax : false;
if (isMaximumButtomScroll && !runningOnMaximumBottomScroll)
{
runningOnMaximumBottomScroll = true;
onMaximumBottomScroll(this, new ScrollEventArgs(ScrollEventType.EndScroll, GetScrollPos(this.Handle, SB_VERT)));
runningOnMaximumBottomScroll = false;
}
break;
case WM_MOUSEHWHEEL:
case WM_MOUSEWHEEL:
isMaximumButtomScroll = GetScrollInfo(m.HWnd, SB_VERT, ref si) ? (si.nPage + si.nPos) >= si.nMax : false;
bool isMouseWheelDown = m.Msg == WM_MOUSEWHEEL ? (int)m.WParam < 0 : (int)m.WParam > 0;
if (isMaximumButtomScroll && isMouseWheelDown && !runningOnMaximumBottomScroll)
{
runningOnMaximumBottomScroll = true;
onMaximumBottomScroll(this, new ScrollEventArgs(ScrollEventType.EndScroll, GetScrollPos(this.Handle, SB_VERT)));
runningOnMaximumBottomScroll = false;
}
break;
case WM_KEYDOWN:
isMaximumButtomScroll = GetScrollInfo(m.HWnd, SB_VERT, ref si) ? (si.nPage + si.nPos) >= (si.nMax - 1) : false;
switch (m.WParam.ToInt32())
{
case (int)Keys.Down:
case (int)Keys.PageDown:
case (int)Keys.End:
if (isMaximumButtomScroll && !runningOnMaximumBottomScroll)
{
runningOnMaximumBottomScroll = true;
onMaximumBottomScroll(this, new ScrollEventArgs(ScrollEventType.EndScroll, GetScrollPos(this.Handle, SB_VERT)));
runningOnMaximumBottomScroll = false;
}
break;
}
break;
}
}
public event ScrollEventHandler onMaximumBottomScroll;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetScrollPos(IntPtr hWnd, int nBar);
[Serializable, StructLayout(LayoutKind.Sequential)]
struct SCROLLINFO
{
public uint cbSize;
public uint fMask;
public int nMin;
public int nMax;
public uint nPage;
public int nPos;
public int nTrackPos;
}
public enum ScrollInfoMask : uint
{
SIF_RANGE = 0x1,
SIF_PAGE = 0x2,
SIF_POS = 0x4,
SIF_DISABLENOSCROLL = 0x8,
SIF_TRACKPOS = 0x10,
SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS),
}
}
Done... enjoy it!
Related
This is the first code I've written in c#, and my first question to Stackoverflow...apologies up front if I'm doing everything wrong! :-/
I've tried to implement the Public Class RTFScrolledToBottom written by LarsTech that was posted as answered the question here:
Get current scroll position from rich text box control?
In the public Form1() code block, this line is generating a CS1061 error:
rtfScrolledBottom1.ScrolledToBottom += rtfScrolledBottom1_ScrolledToBottom;
object does not contain a definition for ScrolledToBottom and no accessible extension method ScrolledToBottom accepting a first argument of type object could be found (are you missing a using directive or an assembly reference?)
Thanks in advance for any assistance pointing me to what I'm screwing up!!
Cheers!
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
private object rtfScrolledBottom1;
public Form1()
{
InitializeComponent();
int rtfScrolledBottom1_ScrolledToBottom = 0;
rtfScrolledBottom1.ScrolledToBottom += rtfScrolledBottom1_ScrolledToBottom;
}
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
}
}
public class RTFScrolledBottom : RichTextBox
{
public event EventHandler ScrolledToBottom;
private const int WM_VSCROLL = 0x115;
private const int WM_MOUSEWHEEL = 0x20A;
private const int WM_USER = 0x400;
private const int SB_VERT = 1;
private const int EM_SETSCROLLPOS = WM_USER + 222;
private const int EM_GETSCROLLPOS = WM_USER + 221;
[DllImport("user32.dll")]
private static extern bool GetScrollRange(IntPtr hWnd, int nBar, out int lpMinPos, out int lpMaxPos);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, Int32 wMsg, Int32 wParam, ref Point lParam);
public bool IsAtMaxScroll()
{
int minScroll;
int maxScroll;
GetScrollRange(this.Handle, SB_VERT, out minScroll, out maxScroll);
Point rtfPoint = Point.Empty;
SendMessage(this.Handle, EM_GETSCROLLPOS, 0, ref rtfPoint);
return (rtfPoint.Y + this.ClientSize.Height >= maxScroll);
}
protected virtual void OnScrolledToBottom(EventArgs e)
{
if (ScrolledToBottom != null)
ScrolledToBottom(this, e);
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (IsAtMaxScroll())
OnScrolledToBottom(EventArgs.Empty);
base.OnKeyUp(e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_VSCROLL || m.Msg == WM_MOUSEWHEEL)
{
if (IsAtMaxScroll())
OnScrolledToBottom(EventArgs.Empty);
}
base.WndProc(ref m);
}
}
}
You have defined rtfScrolledBottom1 as object and object doesn't have any event. You need to define it as RTFScrolledBottom. You also can drop an instance of the RTFScrolledBottom control from toolbox and use it like any other control.
Alternative solution
As an alternative to the solution which you found in the linked post, here is another solution which Works with RichTextBox without creating a derived control, while you can put the logic in a derived control and make it more reusable, like what has done in the linked post.
You can handle VScroll event of the RichTextBox and get the scroll position by calling GetScrollInfo method. Then in the SCROLLINFO if nPage + nPos == nMax, it means the scroll is at bottom:
[StructLayout(LayoutKind.Sequential)]
struct SCROLLINFO {
public int cbSize;
public ScrollInfoMask fMask;
public int nMin;
public int nMax;
public uint nPage;
public int nPos;
public int nTrackPos;
}
public enum ScrollInfoMask : uint {
SIF_RANGE = 0x1,
SIF_PAGE = 0x2,
SIF_POS = 0x4,
SIF_DISABLENOSCROLL = 0x8,
SIF_TRACKPOS = 0x10,
SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS),
}
[DllImport("user32.dll")]
private static extern bool GetScrollInfo(IntPtr hwnd, SBOrientation fnBar,
ref SCROLLINFO lpsi);
public enum SBOrientation : int { SB_HORZ = 0x0, SB_VERT = 0x1 }
private void richTextBox1_VScroll(object sender, EventArgs e)
{
var info = new SCROLLINFO() {
cbSize = (Marshal.SizeOf<SCROLLINFO>()),
fMask = ScrollInfoMask.SIF_ALL
};
GetScrollInfo(richTextBox1.Handle, SBOrientation.SB_VERT, ref info);
if (info.nPage + info.nPos == info.nMax)
{
//VScroll is at bottom
}
}
I would like to use the numlock button for something other than numlock. So basically I would like to turn off numlock when it is pressed and keep it off. I can capture the button press but it still toggles on/off. I want it off, always. Any suggestions?
Not sure WHY anyone would like to know WHY I want this to be done but here is the reason: I have a bluetooth numeric keypad that I want to use to control a machine. Does that justify the question?
After a couple hours of research I came across the following code which did the trick:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
class SetNumlockKeyOn
{
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
internal int type;
internal short wVk;
internal short wScan;
internal int dwFlags;
internal int time;
internal IntPtr dwExtraInfo;
int dummy1;
int dummy2;
internal int type1;
internal short wVk1;
internal short wScan1;
internal int dwFlags1;
internal int time1;
internal IntPtr dwExtraInfo1;
int dummy3;
int dummy4;
}
[DllImport("user32.dll")]
static extern int SendInput(uint nInputs, IntPtr pInputs, int cbSize);
public static void SetNumlockOn()
{
if (Control.IsKeyLocked(Keys.NumLock)) return;
const int mouseInpSize = 28;//Hardcoded size of the MOUSEINPUT tag !!!
INPUT input = new INPUT();
input.type = 0x01; //INPUT_KEYBOARD
input.wVk = 0x90; //VK_NUMLOCK
input.wScan = 0;
input.dwFlags = 0; //key-down
input.time = 0;
input.dwExtraInfo = IntPtr.Zero;
input.type1 = 0x01;
input.wVk1 = 0x90;
input.wScan1 = 0;
input.dwFlags1 = 2; //key-up
input.time1 = 0;
input.dwExtraInfo1 = IntPtr.Zero;
IntPtr pI = Marshal.AllocHGlobal(mouseInpSize * 2);
Marshal.StructureToPtr(input, pI, false);
int result = SendInput(2, pI, mouseInpSize); //Hardcoded size of the MOUSEINPUT tag !!!
//if (result == 0 || Marshal.GetLastWin32Error() != 0)
// Console.WriteLine(Marshal.GetLastWin32Error());
Marshal.FreeHGlobal(pI);
}
}
Here's an example:
public static class NativeMethods
{
public const byte VK_NUMLOCK = 0x90;
public const uint KEYEVENTF_EXTENDEDKEY = 1;
public const int KEYEVENTF_KEYUP = 0x2;
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
public static void SimulateKeyPress(byte keyCode)
{
keybd_event(VK_NUMLOCK, 0x45, KEYEVENTF_EXTENDEDKEY, 0);
keybd_event(VK_NUMLOCK, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
}
}
public partial class Form1 : Form
{
private bool protectKeys; // To protect from inifite keypress chain reactions
public Form1()
{
InitializeComponent();
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (protectKeys)
return;
if (e.KeyCode == Keys.NumLock &&
!(new Microsoft.VisualBasic.Devices.Keyboard().NumLock))
{
protectKeys = true;
NativeMethods.SimulateKeyPress(NativeMethods.VK_NUMLOCK);
protectKeys = false;
}
}
}
I have ListView which sets the width of columns depending on size:
public class CommonListView : ListView
{
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
int columnWidth = (ClientSize.Width - SystemInformation.VerticalScrollBarWidth - 6) / Columns.Count;
foreach (ColumnHeader column in Columns)
column.Width = columnWidth;
}
}
I added listview on a form and set anchor property to 'All' (Top | Bottom | Left | Right).
When I change the size of a form all working correctly. But when I maximize a form (via maximize box) and minimize after that columns have correct size, but appears horizontal scrollbar which should not be.
If I click on it (without changing the size of columns or listview), it will disappear.
What should I do to make this scrollbar did not appear?
The method that I have found to solve my problem described below.
private FormWindowState _previouseFormWindowState;
private void MainForm_Resize(object sender, EventArgs e)
{
if (_previouseFormWindowState == FormWindowState.Maximized && WindowState != FormWindowState.Maximized)
{
var si = new SCROLLINFO();
if (GetScrollInfo(listView, ref si, ScrollBarDirection.SB_HORZ))
{
if (si.nMax < si.nPage)
{
ShowScrollBar(listView.Handle, (int)ScrollBarDirection.SB_HORZ, false);
}
}
}
_previouseFormWindowState = WindowState;
}
public enum ScrollInfoMask : uint
{
SIF_RANGE = 0x1,
SIF_PAGE = 0x2,
SIF_POS = 0x4,
SIF_DISABLENOSCROLL = 0x8,
SIF_TRACKPOS = 0x10,
SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS),
}
public enum ScrollBarDirection
{
SB_HORZ = 0,
SB_VERT = 1,
SB_CTL = 2,
SB_BOTH = 3
}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct SCROLLINFO
{
public uint cbSize;
public uint fMask;
public int nMin;
public int nMax;
public uint nPage;
public int nPos;
public int nTrackPos;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);
public static bool GetScrollInfo(Control ctrl, ref SCROLLINFO si, ScrollBarDirection scrollBarDirection)
{
if (ctrl != null)
{
si.cbSize = (uint)Marshal.SizeOf(si);
si.fMask = (int)ScrollInfoMask.SIF_ALL;
if (GetScrollInfo(ctrl.Handle, (int)scrollBarDirection, ref si))
return true;
}
return false;
}
According to what is written on msdn here, I should get EN_MAXTEXT message, when the number of characters to be inserted would exceed the width of the richTextBox, if it does not have the ES_AUTOHSCROLL style.
But it does not work for me.
I wrote this code:
public class myRTB : RichTextBox
{
protected override void WndProc(ref Message m)
{
if (m.Msg == (WM_REFLECT | WM_COMMAND))
{
int code = (int)m.WParam;
code = (code >> 16) & 0xffff; // convert to hiword
if (code == EN_MAXTEXT)
{
MessageBox.Show("max text");
}
}
base.WndProc(ref m);
}
public const int WM_USER = 0x400;
public const int WM_REFLECT = WM_USER + 0x1C00;
public const int WM_COMMAND = 0x111;
public const int EN_MAXTEXT = 0x0501;
}
And I used this code to remove the ES_AUTOHSCROLL style:
private void button1_Click(object sender, EventArgs e)
{
// get the style
IntPtr style = GetWindowLongPtr32(myRTB1.Handle, GWL_STYLE);
// remove the ES_AUTOHSCROLL style
SetWindowLong32(myRTB1.Handle, GWL_STYLE, (int)style - ES_AUTOHSCROLL);
}
public const int GWL_STYLE = -16;
public const int ES_AUTOHSCROLL = 0x0080;
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", EntryPoint = "GetWindowLong")]
private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int nIndex);
Remove the WM_REFLECT, it's irrelevant for C#.
I'm using p/invoke to call EnableScrollBar from user32.dll (MSDN reference). I noticed that when the scrollbar is enabled, it seems to draw as though no theme is applied and then re-drawn with the theme applied. I've only tested with Windows 7 so far. Is there
any way to stop this from happening?
EDIT: Here's some code to show what happens (dump into a form with scrollbars):
private class Native
{
[DllImport("user32.dll")]
public static extern bool EnableScrollBar(IntPtr hWnd, uint wSBflags, uint wArrows);
public static class SBArrows
{
public const uint ESB_ENABLE_BOTH = 0;
public const uint ESB_DISABLE_BOTH = 3;
public const uint ESB_DISABLE_LEFT = 1;
public const uint ESB_DISABLE_RIGHT = 2;
public const uint ESB_DISABLE_UP = 1;
public const uint ESB_DISABLE_DOWN = 2;
public const uint ESB_DISABLE_LTUP = 1;
public const uint ESB_DISABLE_RTDN = 2;
}
public static class SBFlags
{
public const uint SB_HORZ = 0;
public const uint SB_VERT = 1;
public const uint SB_CTL = 2;
public const uint SB_BOTH = 3;
}
}
private bool Switch = false;
protected override void OnMouseDown(MouseEventArgs e)
{
Native.EnableScrollBar(this.Handle, Native.SBFlags.SB_HORZ, this.Switch ? Native.SBArrows.ESB_DISABLE_BOTH : Native.SBArrows.ESB_ENABLE_BOTH);
this.Switch = !this.Switch;
}
Final Solution
Native.SendMessage(this.Handle, Native.WindowMessages.WM_SETREDRAW, new IntPtr(0), IntPtr.Zero);
Native.EnableScrollBar(this.Handle, Native.SBFlags.SB_HORZ, Native.SBArrows.ESB_ENABLE_BOTH);
Native.SendMessage(this.Handle, Native.WindowMessages.WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);
I don't like this solution much. It does however work:
protected override void OnMouseDown(MouseEventArgs e) {
Native.LockWindowUpdate(this.Handle);
Native.EnableScrollBar(this.Handle, Native.SBFlags.SB_HORZ, this.Switch ? Native.SBArrows.ESB_DISABLE_BOTH : Native.SBArrows.ESB_ENABLE_BOTH);
//this.Invalidate();
Native.LockWindowUpdate(IntPtr.Zero);
this.Switch = !this.Switch;
}