Introduction
Having spent many days and nights searching for a solution to my specific problem: How to append text to a RichTextBox
control without
it automatically scrolling to the bottom of the control, I couldn't find anything that was good for me - I even found a reference to someone
saying that "People have been asking how to do this for over 5 years now and no one has come up with a (good) solution."
Well... I don't know if this qualifies as a "good" solution, but it certainly fulfils my requirements :)
Background
I have been developing an external chat interface for a popular set of MMORPGs, whereby people will be able to talk to their in-game friends
but with the game client minimised. It will eventually enable multi-tabbed chat in a similar fashion to the popular instant messaging programs such as MSN and Yahoo!
Having got everything looking nice, there was still one problem seriously plaguing my development... As my program is designed to allow a user
to see messages which have been posted way earlier than the messages that were still present in the in-game chat interface (limited to 200 messages),
when a user scrolls up to view earlier messages, the arrival of a new message causing the entire control to scroll right back to the bottom was a major annoyance.
I had tried all sorts of things such as logging the current scroll position and then trying to scroll back to there when the auto-scroll stuff happened,
but that resulted in undesirable 'flapping around' of the text content.
Another solution suggested temporarily setting the focus to another control, appending the text to the main RichTextBox
, then returning focus to the RTB (the auto-scrolling
doesn't happen if the RichTextBox
doesn't have focus). However, this was just unbearable to look at once there was sufficient text content to really slow everything down.
I even tried implementing the chat interface in a WebBrowser
control which alleviated the auto-scroll nightmare, but seriously restricted my other options.
Then... I decided to delve deeper into the WndProc
message system and managed to single out the culprit for the auto-trolling problem - WM_SETFOCUS
.
Using the Code
The solution I have arrived at is effectively an inherited RichTextBox
class which disables the WndProc
message that triggers the RTB control to gain focus,
then simulates some of the functionality subsequently lost due to disabling it.
protected override void WndProc(ref Message m)
{
if(m.Msg != WM_SETFOCUS)
base.WndProc(ref m);
}
There are of course a few drawbacks to this... If there is no focus, text can not be selected, copied, etc. To regain this lost functionality,
some event handlers need to be added to simulate the original disabled functions. Well... this example includes a few functions which allow the user to select text again.
Copying text to the clipboard can be achieved by adding event handlers to the various key event handlers (an example of copying to clipboard is shown later).
So, here is my new MyRichTextBox
class!
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Forms;
class MyRichTextBox : RichTextBox
{
public bool mouseDown;
public int selCurrent;
public int selOrigin;
public int selStart;
public int selEnd;
public int selTrough;
public int selPeak;
private Color defaultBackColour;
private int WM_SETFOCUS = 0x0007;
private UInt32 EM_SETSEL = 0x00B1;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);
public MyRichTextBox()
{
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.rtb_MouseDown);
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.rtb_MouseMove);
this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.rtb_MouseUp);
defaultBackColour = this.BackColor;
}
protected override void WndProc(ref Message m)
{
if(m.Msg != WM_SETFOCUS)
base.WndProc(ref m);
}
public void rtb_MouseDown(object sender, MouseEventArgs e)
{
mouseDown = true;
selOrigin = selStart = selEnd = selPeak = selTrough = GetCharIndexFromPosition(e.Location);
highlightSelection(1, Text.Length - 1, false);
}
public void rtb_MouseUp(object sender, MouseEventArgs e)
{
mouseDown = false;
}
public void rtb_MouseMove(object sender, MouseEventArgs e)
{
if (mouseDown)
{
selCurrent = GetCharIndexFromPosition(e.Location);
if (selCurrent < selOrigin + 1)
{
if (selCurrent > selTrough)
{
highlightSelection(selTrough, selCurrent, false);
}
selTrough = selCurrent;
selEnd = selOrigin + 1;
selStart = selCurrent;
}
else
{
if (selCurrent < selPeak)
{
highlightSelection(selPeak, selCurrent, false);
}
selPeak = selCurrent;
selStart = selOrigin;
selEnd = selCurrent;
}
highlightSelection(selStart, selEnd);
}
}
private void highlightSelection(int start, int end, bool highlight = true)
{
selectText(start, end);
SelectionBackColor = highlight ? Color.LightBlue : defaultBackColour;
}
private void selectText(int start, int end)
{
SendMessage(Handle, EM_SETSEL, start, end);
}
}
Points of Interest
Of course, there are various other functionalities that are lost due to disabling the WM_SETFOCUS
message, for example, as previously mentioned,
the ability to copy text to the clipboard. However, this can simply be fixed by adding an event handler to your main form:
private void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == 0x03) {
Clipboard.SetText(rtb1.Text.Substring(rtb1.selStart, rtb1.selEnd - rtb1.selStart));
}
}
For anything else, I'm afraid you're on your own, but hopefully if you've found your way to this article, this will give you a nice starting point.
I make no claims of being a professional programmer... I just dabble in my spare time, so please go easy on me for my first article :)
History
- 5 April 2012 - First release.