Make MouseWheel event pass from child to parent - c#

I have a Panel where AutoScroll is true. This panel contains many smaller panels that fill all available space like tiles. When there are too many sub-panels to display I get the vertical scroll bar as expected.
Each of these "tiles" have some event handlers tied to them to handle MouseDown / MouseUp / MouseMove as they can be dragged around.
The problem I'm having is that mouse wheel scroll doesn't work on the parent panel as it won't have focus. I can't give it focus because in all probability I'll be scrolling while moving a sub-panel which will have the focus instead, and even then that would require workarounds since panels don't like focus.
I've been trying (and failing) to find a way to propagate only mousewheel events from the children to the parent.
I've read that in Winforms if a control cannot handle a mouse event it will bubble it up to the parent of that control, and then to the parent of that control and so on until it finds a suitable handler.
With this in mind I figured the best solution would be to use WndProc to override all scroll related events on the sub-panel and pass them to the parent while leaving all other events intact but admittedly this isn't my strong suit and I'm lost.
I've tried a few other solutions such as making the sub-panels invisible to all mouse events but as you might have guessed this was bad. I've read about implementing a message filter but didn't understand it.
Here's the code that will give you a very basic example of the panel and its children:
private void Form1_Load(object sender, EventArgs e)
{
Height = 600;
Width = 300;
Color[] colors = new Color[]{ Color.PowderBlue, Color.PeachPuff };
Panel panel = new Panel()
{
Height = this.ClientSize.Height - 20,
Width = 200,
Top = 10,
Left = 10,
BackColor = Color.White,
BorderStyle = BorderStyle.FixedSingle,
AutoScroll = true
};
for (int i = 0; i < 10; i++)
{
Panel subPanel = new Panel()
{
Name = #"SubPanel " + i.ToString(),
Height = 100,
Width = panel.Width - System.Windows.Forms.SystemInformation.VerticalScrollBarWidth - 2,
BackColor = colors[i % 2],
Top = i * 100
};
subPanel.MouseClick += subPanel_MouseClick;
panel.Controls.Add(subPanel);
}
Controls.Add(panel);
}
void subPanel_MouseClick(object sender, MouseEventArgs e)
{
Panel panel = sender as Panel;
Text = panel.Name;
}
Here's my attempt at overriding WndProc in a custom panel:
class NoScrollPanel : Panel
{
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int MOUSEWHEEL = 0x020A;
private const int KEYDOWN = 0x0100;
protected override void WndProc(ref Message m)
{
if ((m.HWnd == Handle) && (m.Msg == MOUSEWHEEL || m.Msg == WM_VSCROLL || (m.Msg == KEYDOWN && (m.WParam == (IntPtr)40 || m.WParam == (IntPtr)35))))
{
PostMessage(Parent.Handle, m.Msg, m.WParam, m.LParam);
}
else
{
base.WndProc(ref m);
}
}
[DllImport("User32.dll")]
private static extern IntPtr PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
}
Any help or alternative approaches are most welcome. Thanks!

At my side #a-clymer's solution does not work, perhaps different environment. Currently there is no direct, clear answer for my problem so I try to combine several thoughts of other professionals and succeed.
In my current project, I have a few input controls contained in a Panel. I managed to make the mouse wheel scroll the panel instead of the ComboBox items by creating a child class of ComboBox and override its WndProc:
public class ComboBoxWithParentMouseWheel : ComboBox
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
const int WM_MOUSEWHEEL = 0x020A;
//thanks to a-clymer's solution
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MOUSEWHEEL)
{
//directly send the message to parent without processing it
//according to https://stackoverflow.com/a/19618100
SendMessage(this.Parent.Handle, m.Msg, m.WParam, m.LParam);
m.Result = IntPtr.Zero;
}else base.WndProc(ref m);
}
}

All credit goes to Hans Passant (again), taken from the thread he suggested: https://stackoverflow.com/a/3562449/17034
Allowing the the containing panel to take focus worked fine. For the demo code above the class needs no changes, just use it for only the containing panel. I had to make some tweaks to my project to call focus when necessary, but it was far from rocket science.
Thanks again.

I could not get any of these solutions to work. I am overriding the WndProc fxn just like you. Finally found the solution! I noticed that if the container holding my TextBox was in focus, then the scroll worked, just not when the TextBox was in focus.
I didn't need to change the transparency of the event, I needed to send the event to a different Control Handle! (It seems so simple now, but I've trying to figure this out for days!)
internal class myTextBox : TextBox
{
const int WM_MOUSEWHEEL = 0x020A;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MOUSEWHEEL)
m.HWnd = this.Parent.Handle; // Change the Handle of the message
base.WndProc(ref m);
}
}
I haven't seen any mention of this method in all my Google searches, if there is some reason NOT to do this I hope someone will reply.

An improved version of #a-clymer version that works better.
const int WM_MOUSEWHEEL = 0x020A;
if (m.Msg == WM_MOUSEWHEEL)
{
// find the first scrollable parent control
Control p = this;
do
{
p = p.Parent;
} while (p != null && !(p is ScrollableControl));
// rewrite the destination handle of the message
if (p != null)
m.HWnd = p.Handle;
}

Related

Move window without a mouse drag gesture

I have a window (subclass of System.Windows.Forms.Form) without a border in order to apply a custom style to it. Moving the window when the mouse button is down seems to be easy. Either by sending a WM_NCLBUTTONDOWN HT_CAPTION or a WM_SYSCOMMAND 0xF102 message the window can be dragged to a new location. As soon as the mouse button is up though, it seems to be impossible to move the window.
One could send WM_SYSCOMMAND SC_MOVE message but then the cursor moves at the top center of the window and awaits for the user to press any arrow key in order to hook the window for move -which is awkward at the least. I tried to fake a key press/release sequence but that of course didn't work as I called SendMessage with the current form Handle as argument but I guess the message should not be sent to the current form.
The desired behavior is: click a button (ie the mouse button is released) move the form where cursor goes, click again to release the form. Is that possible with winapi? Unfortunately I am not familiar with it.
Addendum
Sending a key input: I tried to use SendInput as SendMessage is supposed to be bad practice. Still it didn't hook the window. I tried to read and print the winapi error code with Marshal.GetLastWin32Error() and I got a 5 which is access denied. The curious thing was that I received the messages after the move sequence ended (ie I manually pressed a key or mouse button). No idea how to work around this.
Using the IMessageFilter (IVSoftware's answer): this is what I ended up doing but that has 2 issues: moving the window with its Location property has lag compared to the native way (no big deal for now) and also it doesn't receive mouse messages that occur outside the main form. That means it won't work a. for multiscreen environments b. if the cursor moves outside the forms of the application. I could create full screen transparent forms for every monitor that will serve as an "message canvas" but still... why not give the OS way a chance.
As I understand it, the desired behavior is to enable the "Click to Move" (one way or another) and then click anywhere on a multiscreen surface and have the borderless form follow the mouse to the new position. One solution that seems to work in my brief testing is to pinvoke the WinApi SetWindowsHookEx to install a global low level hook for WH_MOUSE_LL in order to intercept WM_LBUTTONDOWN.
*This answer has been modified in order to track updates to the question.
Low-level global mouse hook
public MainForm()
{
InitializeComponent();
using (var process = Process.GetCurrentProcess())
{
using (var module = process.MainModule!)
{
var mname = module.ModuleName!;
var handle = GetModuleHandle(mname);
_hook = SetWindowsHookEx(
HookType.WH_MOUSE_LL,
lpfn: callback,
GetModuleHandle(mname),
0);
}
}
// Unhook when this `Form` disposes.
Disposed += (sender, e) => UnhookWindowsHookEx(_hook);
// A little hack to keep window on top while Click-to-Move is enabled.
checkBoxEnableCTM.CheckedChanged += (sender, e) =>
{
TopMost = checkBoxEnableCTM.Checked;
};
// Compensate move offset with/without the title NC area.
var offset = RectangleToScreen(ClientRectangle);
CLIENT_RECT_OFFSET = offset.Y - Location.Y;
}
readonly int CLIENT_RECT_OFFSET;
IntPtr _hook;
private IntPtr callback(int code, IntPtr wParam, IntPtr lParam)
{
var next = IntPtr.Zero;
if (code >= 0)
{
switch ((int)wParam)
{
case WM_LBUTTONDOWN:
if (checkBoxEnableCTM.Checked)
{
_ = onClickToMove(MousePosition);
// This is a very narrow condition and the window is topmost anyway.
// So probably swallow this mouse click and skip other hooks in the chain.
return (IntPtr)1;
}
break;
}
}
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}
}
Perform the move
private async Task onClickToMove(Point mousePosition)
{
// Exempt clicks that occur on the 'Enable Click to Move` button itself.
if (!checkBoxEnableCTM.ClientRectangle.Contains(checkBoxEnableCTM.PointToClient(mousePosition)))
{
// Try this. Offset the new `mousePosition` so that the cursor lands
// in the middle of the button when the move is over. This feels
// like a semi-intuitive motion perhaps. This means we have to
// subtract the relative position of the button from the new loc.
var clientNew = PointToClient(mousePosition);
var centerButton =
new Point(
checkBoxEnableCTM.Location.X + checkBoxEnableCTM.Width / 2,
checkBoxEnableCTM.Location.Y + checkBoxEnableCTM.Height / 2);
var offsetToNow = new Point(
mousePosition.X - centerButton.X,
mousePosition.Y - centerButton.Y - CLIENT_RECT_OFFSET);
// Allow the pending mouse messages to pump.
await Task.Delay(TimeSpan.FromMilliseconds(1));
WindowState = FormWindowState.Normal; // JIC window happens to be maximized.
Location = offsetToNow;
}
checkBoxEnableCTM.Checked = false; // Turn off after each move.
}
In the code I used to test this answer, it seemed intuitive to center the button where the click takes place (this offset is easy to change if it doesn't suit you). Here's the result of the multiscreen test:
WinApi
#region P I N V O K E
public enum HookType : int { WH_MOUSE = 7, WH_MOUSE_LL = 14 }
const int WM_LBUTTONDOWN = 0x0201;
delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(HookType hookType, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam,
IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
#endregion P I N V O K E
Here is a possible solution that I will go with after all. It's not that IVSoftware's answer doesn't work, it does, I tried it. It's that my solution has some set of advantages relevant to what I am trying to do. The main points are:
Utilizing the IMessageFilter (thanks to SwDevMan81's answer) which reminded me that the correct way to process messages "globally" is not to override WndProc)
Laying out a set of transparent windows on all screens in order to receive mouse move messages everywhere.
Pros
It works without having to make any P/Invokes
It allows more tricks to be done like for example leverage the transparent forms to implement a "move border instead of form" functionality (though I didn't test it, paint might be tricky)
Can be easily applied for resize as well.
Can work with mouse buttons other than the left/primary.
Cons
It has too many "moving parts". At least for my taste. Laying out transparent windows all over the place? Hm.
It has some corner cases. Pressing Alt+F4 while moving the form will close the "canvas form". That can be easily mitigated but there might be others as well.
There must be an OS way to do this...
The code (basic parts; full code on github)
public enum WindowMessage
{
WM_MOUSEMOVE = 0x200,
WM_LBUTTONDOWN = 0x201,
WM_LBUTTONUP = 0x202,
WM_RBUTTONDOWN = 0x204,
//etc. omitted for brevity
}
public class MouseMessageFilter : IMessageFilter
{
public event EventHandler MouseMoved;
public event EventHandler<MouseButtons> MouseDown;
public event EventHandler<MouseButtons> MouseUp;
public bool PreFilterMessage(ref Message m)
{
switch (m.Msg)
{
case (int)WindowMessage.WM_MOUSEMOVE:
MouseMoved?.Invoke(this, EventArgs.Empty);
break;
case (int)WindowMessage.WM_LBUTTONDOWN:
MouseDown?.Invoke(this, MouseButtons.Left);
break;
//etc. omitted for brevity
}
return false;
}
}
public partial class CustomForm : Form
{
private MouseMessageFilter windowMoveHandler = new();
private Point originalLocation;
private Point offset;
private static List<Form> canvases = new(SystemInformation.MonitorCount);
public CustomForm()
{
InitializeComponent();
windowMoveHandler.MouseMoved += (_, _) =>
{
Point position = Cursor.Position;
position.Offset(offset);
Location = position;
};
windowMoveHandler.MouseDown += (_, button) =>
{
switch (button)
{
case MouseButtons.Left:
EndMove();
break;
case MouseButtons.Middle:
CancelMove();
break;
}
};
moveButton.MouseClick += (_, _) =>
{
BeginMove();
};
}
private void BeginMove()
{
Application.AddMessageFilter(windowMoveHandler);
originalLocation = Location;
offset = Invert(PointToClient(Cursor.Position));
ShowCanvases();
}
//Normally an extension method in another library of mine but I didn't want to
//add a dependency just for that
private static Point Invert(Point p) => new Point(-p.X, -p.Y);
private void ShowCanvases()
{
for (int i = 0; i < Screen.AllScreens.Length; i++)
{
Screen screen = Screen.AllScreens[i];
Form form = new TransparentForm
{
Bounds = screen.Bounds,
Owner = Owner
};
canvases.Add(form);
form.Show();
}
}
private void EndMove()
{
DisposeCanvases();
}
private void DisposeCanvases()
{
Application.RemoveMessageFilter(windowMoveHandler);
for (var i = 0; i < canvases.Count; i++)
{
canvases[i].Close();
}
canvases.Clear();
}
private void CancelMove()
{
EndMove();
Location = originalLocation;
}
//The form used as a "message canvas" for moving the form outside the client area.
//It practically helps extend the client area. Without it we won't be able to get
//the events from everywhere
private class TransparentForm : Form
{
public TransparentForm()
{
StartPosition = FormStartPosition.Manual;
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
}
protected override void OnPaintBackground(PaintEventArgs e)
{
//Draws a white border mostly useful for debugging. For example that's
//how I realised I needed Screen.Bounds instead of WorkingArea.
ControlPaint.DrawBorder(e.Graphics, new Rectangle(Point.Empty, Size),
Color.White, 2, ButtonBorderStyle.Solid,
Color.White, 2, ButtonBorderStyle.Solid,
Color.White, 2, ButtonBorderStyle.Solid,
Color.White, 2, ButtonBorderStyle.Solid);
}
}
}

Disabling maximizing form in C# [duplicate]

I'm annoyed that the I'm promised a fixed window that the user can't resize, but then of course they're allowed to double click the title bar to maximize this 'unresizable' window. How can I turn this off? Can I do it with winforms code, or must I go down to Win32?
Thanks!
You could set the MaximizeBox property of the form to false
You can disable the double-click message on a title bar in general (or change the default behavior which is maximizing the window). it works on any FormBorderStyle:
private const int WM_NCLBUTTONDBLCLK = 0x00A3; //double click on a title bar a.k.a. non-client area of the form
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_NCLBUTTONDBLCLK)
{
m.Result = IntPtr.Zero;
return;
}
base.WndProc(ref m);
}
MSDN Source
Cheers!
///
/// This is we are overriding base WIN32 window procedure to prevent the form from being moved by the mouse as well as resized by the mouse double click.
///
///
protected override void WndProc(ref Message m)
{
const int WM_SYSCOMMAND = 0x0112;
const int SC_MOVE = 0xF010;
const int WM_NCLBUTTONDBLCLK = 0x00A3; //double click on a title bar a.k.a. non-client area of the form
switch (m.Msg)
{
case WM_SYSCOMMAND: //preventing the form from being moved by the mouse.
int command = m.WParam.ToInt32() & 0xfff0;
if (command == SC_MOVE)
return;
break;
}
if(m.Msg== WM_NCLBUTTONDBLCLK) //preventing the form being resized by the mouse double click on the title bar.
{
m.Result = IntPtr.Zero;
return;
}
base.WndProc(ref m);
}
I know I'm late to the party, May help someone who is searching for the same.
private const int WM_NCLBUTTONDBLCLK = 0x00A3; //double click on a title bar a.k.a. non-client area of the form
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_NCLBUTTONDBLCLK: //preventing the form being resized by the mouse double click on the title bar.
handled = true;
break;
default:
break;
}
return IntPtr.Zero;
}
I just checked it in VB.Net. Below code worked for me.
Private Const Win_FormTitleDoubleClick As Integer = 163
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = Win_FormTitleDoubleClick Then
m.Result = IntPtr.Zero
Return
End If
MyBase.WndProc(m)
End Sub
Note: 163 is the event code
I approached the problem slightly differently. First I removed the minimize and maximize options from the control box via the MaximizeBox and MinimizeBox properties as you'd expect.
Then I added the following OnResizeEnd() event and attached it to the Form's ResizeEnd event handler:
/// <summary>Locks the form to fill the screen that it's placed on and remain in that state as long as it's open.</summary>
private void GuiMain_ResizeEnd( object sender, EventArgs e )
{
this.Location = Screen.WorkingArea.Location;
this.Size = Screen.WorkingArea.Size;
this.MaximizedBounds = Screen.WorkingArea;
this.MinimumSize = Screen.WorkingArea.Size;
this.WindowState = FormWindowState.Normal;
}
This solution necessarily relies on the existence of the following accessor which you can copy, or you can simply replace each instance of Screen. in the above with Screen.FromHandle( this.Handle ).
protected Screen Screen => Screen.FromHandle( this.Handle );
Obviously, if ironically, this actually keeps the form in the FormWindowState.Normal state, but it mimics the effect of maximization, and resets this fullscreen state after any attempt to change it.
Interestingly, due to the use of the Screen.FromHandle() settings (as opposed to hard-coded ones), you can actually drag the form from one display to another, whereupon it immediately "snaps" to fill THAT screen instead. Something I found quite handy, but which may require additional code to correct if you don't want that functionality in your application.

c# scrolling using mousewheel without scrollbar

I'm trying to dynamically change the view of a panel that contains buttons in my winforms application using the mousewheel. I'm basically changing the location of the panel using the code below however whenever I use the mousewheel to scroll, the scrollbars can be seen. They sort of flask/ flicker into view when I change the location. Is there any way to stop the scrollbars from appearing?
location -= 40;
this.pnl.VerticalScroll.Value = location;
this.pnl.AutoScrollPosition = new Point(0, location);
You cannot simple change it from winforms, since it is being automatically displayed by windows. There is a not so great solution for this issue.I had a similar requirement and what I used was this :
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
private enum ScrollBarDirection
{
SB_HORZ = 0,
SB_VERT = 1,
SB_CTL = 2,
SB_BOTH = 3
}
protected override void WndProc(ref System.Windows.Forms.Message m)
{
ShowScrollBar(panel1.Handle, (int)ScrollBarDirection.SB_VERT, false);
base.WndProc(ref m);
}
Import the win32 dll user32.dll
Call the ShowScrollBar method in it with required parameters as shown in above example

How do I redirect mouse wheel messages from one window to another?

WM_MOUSEWHEEL messages are sent to the control with the focus. My application has a complex control hierarchy, with controls containing other controls, some of which are invisible or overlapping. I would like the mouse wheel to scroll a specific ScrollableControl.
This question has an answer with a IMessageFilter implementation that catches WM_MOUSEWHEEL messages. This works well and I see the messages being caught. I tried manipulating ScrollableControl's VerticalScroll property to scroll its contents, by changing the value of VerticalScroll.Value. Unfortunately, there are some undesirable side effects like the mouse thumb in the scrollbar becoming unsynchronized with the ScrollableControl's contents. Perhaps this is because this work is being done inside the message pump instead of in an event handler.
This post describes a technique where WM_MOUSEWHEEL messages are reposted to another window. I would like to implement a IMessageFilter that catches WM_MOUSEWHEEL messages, and forwards them to a designated recipient.
I create the following IMessageFilter that tries to do this. I can see the forwarded message being caught by my filter, and I return false from the filter to tell the control to handle the message. The target control does not receive an OnMouseWheel event.
Can this filter be modified to to allow my targetControl to be scrolled using redirected messages?
public static class MouseWheelMessageRedirector
{
public static void Add(Control rootControl, ScrollableControl targetControl)
{
var filter = new MouseWheelMessageFilter(rootControl, targetControl);
Application.AddMessageFilter(filter);
rootControl.Disposed += (sender, args) => Application.RemoveMessageFilter(filter);
targetControl.MouseWheel += (sender, args) =>
{
// ... this code never executes
System.Diagnostics.Trace.WriteLine("WHEEL ON TARGET");
};
}
private class MouseWheelMessageFilter : IMessageFilter
{
const int WM_MOUSEWHEEL = 0x020A;
[DllImport("user32.dll", SetLastError = true)]
static extern bool PostMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
public MouseWheelMessageFilter(Control rootControl, ScrollableControl targetControl)
{
_rootControl = rootControl;
_targetControl = targetControl;
_targetWindowHandle = _targetControl.Handle;
}
public bool PreFilterMessage(ref Message m)
{
if (m.Msg != WM_MOUSEWHEEL)
return false;
if (m.HWnd == _targetWindowHandle)
return false;
// ... get the control that the mouse is over
// ... determine if this is a control that we want to handle the message for
// ... (omitted)
PostMessage(_targetWindowHandle, m.Msg, m.WParam, m.LParam);
return true;
}
private Control _rootControl;
private ScrollableControl _targetControl;
private IntPtr _targetWindowHandle;
}
}
I just did this same thing. Here's what I did:
public bool PreFilterMessage(ref Message m)
{
if ((WM)m.Msg == WM.MOUSEWHEEL)
{
// if mouse is over a certain component, prevent scrolling
if (comboBoxVendors.Bounds.Contains(PointToClient(Cursor.Position)))
{
// return true which says the message is already processed
return true;
}
// which direction did they scroll?
int delta = 0;
if ((long)m.WParam >= (long)Int32.MaxValue)
{
var wParam = new IntPtr((long)m.WParam << 32 >> 32);
delta = wParam.ToInt32() >> 16;
}
else
{
delta = m.WParam.ToInt32() >> 16;
}
delta = delta*-1;
var direction = delta > 0 ? 1 : 0;
// post message to the control I want scrolled (I am converting the vertical scroll to a horizontal, bu you could just re-send the same message to the control you wanted
PostMessage(splitContainerWorkArea.Panel2.Handle, Convert.ToInt32(WM.HSCROLL), (IntPtr) direction, IntPtr.Zero);
// return true to say that I handled the message
return true;
}
// message was something other than scroll, so ignore it
return false;
}
Also, I used the windows message enum from PInvoke.net:
http://www.pinvoke.net/default.aspx/Enums.WindowsMessages
Finally, be sure to remove the message filter when you close the form, or when you no longer need to process the messages:
Application.RemoveMessageFilter(thisForm)

Disable Painting of the VScrollbar in a System.Windows.Forms.RichTextBox

I have a custom control inherited from RichTextBox.
This control has the ability to "disable" rich text editing.
I achive this by just setting the Rtf property to the text property during the TextChanged event.
this is how my code looks like:
private bool lockTextChanged;
void RichTextBox_TextChanged(object sender, EventArgs e)
{
// prevent StackOverflowException
if (lockTextChanged) return;
// remember current position
int rtbstart = rtb.SelectionStart;
int len = rtb.SelectionLength;
// prevent painting
rtb.SuspendLayout();
// set the text property to remove the entire formatting.
lockTextChanged = true;
rtb.Text = rtb.Text;
rtb.Select(rtbstart, len);
lockTextChanged = false;
rtb.ResumeLayout(true);
}
That worked well. However in a large text with like 200 lines the controls jitters (you see the first lines of text for the wink).
To prevent that from happening I filter the WM_PAINT between SuspendLayout() and ResumeLayout()
private bool layoutSuspended;
public new void SuspendLayout()
{
layoutSuspended = true;
base.SuspendLayout();
}
public new void ResumeLayout()
{
layoutSuspended = false;
base.ResumeLayout();
}
public new void ResumeLayout(bool performLayout)
{
layoutSuspended = false;
base.ResumeLayout(performLayout);
}
private const int WM_PAINT = 0x000F;
protected override void WndProc(ref System.Windows.Forms.Message m)
{
if (!(m.Msg == WM_PAINT && layoutSuspended))
base.WndProc(ref m);
}
that did the trick, the RichTextBox isn't jittering anymoe.
That's what I wanted to achive, except one thing:
The scrollbar is still jittering everytime I type text to my control.
Now my question:
Does anyone have a clue for me how to prevent the scrollbar from redrawing during Suspend/Resume Layout?
SuspendLayout() isn't going to have an effect, there are no child controls inside an RTB that need to be arranged. RTB is missing the Begin/EndUpdate() methods that most controls have, although it supports it. It suspends painting, although I'm not so sure it suspends updates to the scrollbar. Add them as follows:
public void BeginUpdate() {
SendMessage(this.Handle, WM_SETREDRAW, (IntPtr)0, IntPtr.Zero);
}
public void EndUpdate() {
SendMessage(this.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero);
}
// P/invoke declarations
private const int WM_SETREDRAW = 0xb;
[System.Runtime.InteropServices.DllImport("user32.dll")]
private extern static IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
The better way to prevent the user from editing text is to set the ReadOnly property to True. Removing the scrollbar entirely is possible too by overriding CreateParams.

Categories

Resources