Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Basic

A Full Featured Editor Based on the Richtext Box

5.00/5 (24 votes)
14 Feb 2022CPOL8 min read 77.2K   12K  
Creating a reusable editor for C# Applications using the RichTextBox Control
The RichtextBox class in C# is used as the basis for a more complete stand-alone editor component that can be added to desktop applications

Image 1

 

Image 2

 

Image 3

Introduction

The RichTextBox control (RTB) in the NET Framework offers a simple way to edit richtext markup code, used in RTF files and older MS-Word documents, as well as simple text, in a WYSIWYG manner. However, in its basic form, it lacks the features of a more complete editor, such as Find and Replace, document printing, page layout, and file and image drag and drop.

Using the Code

This project began as an effort to make the base RichTextBox class into a more full featured editor that could serve as a reusable component for other applications I was developing. While the RTB has many useful properties and methods, it is not a full featured editor in its basic form. In the course of adding what I felt were necessary improvements, the following were the main problems I encountered, along with their solutions from many different sources on the net:

  1. Printing - There is no built-in way to print the contents of an RTB. I created a PrintHelper class with a GeneralPrintForm constructor, available as a separate "Tips & Tricks" post here: https://www.codeproject.com/Tips/829368/A-Simple-RTF-Print-Form". Note that the Net PrintDialog also provides a PrintPreview window which shows how your document will print. The PrintHelper class uses the default Print and Preview Dialogs.
  2. Page Layout and Margins - A relatively simple problem with the appearance of an RTB on a form is the absence of a visual margin for its contents. The easy way to solve this is enclose RTB in a slightly larger Panel container with a FixedSingle BorderStyle, so there is an apparent margin between the scrolling text in the control and the surrounding edges of the panel. Note that this only simulates the margins of the printed page. To set the actual Margins, Header and Footer for the printed page, using the GeneralPrintForm from the PrintHelper Class in Item 1 above, a System.Drawing.Printing.PageSettings object is filled in and sent to the GeneralPrintForm via its constructor:
    C#
    // PRINT MENU OPTION
           private void printToolStripMenuItem_Click(object sender, EventArgs e)
           {
               string finalheader = HeaderString;
    
               Exception ex = new Exception("An Error occurred while Printing");
               System.Drawing.Printing.PageSettings PSS = new System.Drawing.Printing.PageSettings();
               PSS.Margins.Left = (int)(LeftMargin * 100);
               PSS.Margins.Right = (int)(RightMargin * 100);
               PSS.Margins.Top = (int)(TopMargin * 100);
               PSS.Margins.Bottom = (int)(BottomMargin * 100);
               PSS.Landscape = LandScapeModeOn;
               if (HeaderOn)
               {
                   if(AddFileNameToHeader)
                   {
                       finalheader += " " + FileName;
                   }
                   if(AddDateToHeader)
                   {
                       finalheader += " Printed: " + System.DateTime.Today.ToShortDateString() +
                                      " " + System.DateTime.Now.ToShortTimeString();
                   }
               }
    
               rtt.GeneralPrintForm("Print Document", rtbMainForm1.Rtf,
                        ref ex, PSS, HeaderOn, finalheader, HeaderFont, HeaderNumberPages);
           }
    

    I created a simple form to collect these settings from the user within the editor:

    Image 4

    C#
     public partial class marginsForm : Form
        {
            public marginsForm()
            {
                InitializeComponent();
            }
            // OVERRIDE 1
            public marginsForm(float Top,float Bottom, float Left, float Right,bool landscapemodeon)
            {
                InitializeComponent();
                top = Top;
                bottom = Bottom;
                left = Left;
                right = Right;
                landscapemode = landscapemodeon;
            }
            // OVERRIDE 2
            public marginsForm(float Top, float Bottom, float Left, float Right,
            bool landscapemodeon,string headerstring,Font headerfont,bool headeron,bool numberpages,
               bool adddatetoheader, bool addfilenametoheader)
            {
                InitializeComponent();
                top = Top;
                bottom = Bottom;
                left = Left;
                right = Right;
                landscapemode = landscapemodeon;
                this.headerfont = headerfont;
                this.headeron = headeron;
                this.pagenumberson = numberpages;
                this.headerstring = headerstring;
                this.adddatetoheader = adddatetoheader;
                this.addfilenametoheader = addfilenametoheader;
            }
    
            // PUBLIC ACCESSORS
            public bool ResultOk
            {
                get
                {
                    return resultok;
                }
            }
            public float Top
            {
                get
                {
                    return top;
                }
                set
                {
                    top = value;
                }
            }
            public float Bottom
            {
                get
                {
                    return bottom;
                }
                set
                {
                    bottom = value;
                }
            }
            public float Left
            {
                get
                {
                    return left;
                }
                set
                {
                    left = value;
                }
            }
            public float Right
            {
                get
                {
                    return right;
                }
                set
                {
                    right = value;
                }
            }
            public bool LandscapeMode
            {
                get
                {
                    return landscapemode;
                }
                set
                {
                    landscapemode = value;
                }
            }
            public Font HeaderFont
            {
                get
                {
                    return headerfont;
                }
            }
            public bool HeaderOn
            {
                get
                {
                    return headeron;
                }
                set
                {
                    headeron = value;
                }
            }
            public bool PageNumbersOn
            {
                get
                {
                    return pagenumberson;
                }
                set
                {
                    pagenumberson = value;
                }
            }
            public string DocumentFilename
            {
                get
                {
                    return documentfilename;
                }
                set
                {
                    documentfilename = value;
                }
            }
            public string HeaderString
            {
                get
                {
                    return headerstring;
                }
                set
                {
                    headerstring = value;
                }
            }
            public bool AddDateToHeader
            {
                get
                {
                    return adddatetoheader;
                }
                set
                {
                    adddatetoheader = value;
                }
            }
            public bool AddFileNameToHeader
            {
                get
                {
                    return addfilenametoheader;
                }
                set
                {
                    addfilenametoheader = value;
                }
            }
    
            // PRIVATE VARIABLES
            private float top, bottom, left, right = 0.0f;
            private bool landscapemode = false;
            private Font headerfont = new Font("Arial", 10); // DEFAULT
            private bool headeron = false;
            private bool pagenumberson = false;
            private bool adddatetoheader = false;
            private bool addfilenametoheader = false;
            private string documentfilename = string.Empty;
            private string headerstring = string.Empty;
            ExceptionHandlerTools eht = new ExceptionHandlerTools();
            // OK Button
            private void button1_Click(object sender, EventArgs e)
            {
                try
                {
                    top = (float)Convert.ToDouble(tbTop.Text);
                    bottom = (float)Convert.ToDouble(tbBottom.Text);
                    left = (float)Convert.ToDouble(tbLeft.Text);
                    right = (float)Convert.ToDouble(tbRight.Text);
                }
                catch
                {
                    eht.GeneralExceptionHandler("Enter Margins as 3 digit decimals",
                                                "(60) Set Margins", false, null);
                    return;
                }
                if ((top == 0 || bottom == 0 || left == 0 || right == 0) && headeron)
                {
                    Exception ex = new Exception("(Application) - You must set the margins
                                   for the header to print correctly\r\n"+
                        "Either set the margins to values greater 
                         than 0 or turn the header option off");
                    eht.GeneralExceptionHandler("Header will not print if margins are set to 0",
                                                "Page Layout", false, ex);
                    return;
                }
                if (rbMarginsFormLandscape.Checked)
                {
                    landscapemode = true;
                }
                else
                {
                    landscapemode = false;
                }
                if (headeron)
                {
                    headerstring = tbHeaderText.Text;
                }
    
                resultok = true;
                this.Close();
            }
            // SET MARGINS TO 0
            private void button3_Click(object sender, EventArgs e)
            {
                tbTop.Text = "0.0";
                tbBottom.Text = "0.0";
                tbRight.Text = "0.0";
                tbLeft.Text = "0.0";
            }
            // SET MARGINS TO 1" ALL AROUND
            private void btnOneInch_Click(object sender, EventArgs e)
            {
                tbTop.Text = "1.0";
                tbBottom.Text = "1.0";
                tbRight.Text = "1.0";
                tbLeft.Text = "1.0";
            }
            // HEADER ON BUTTON CHECK CHANGED
            private void rbHeaderOn_CheckedChanged(object sender, EventArgs e)
            {
                if (rbHeaderOn.Checked)
                {
                    headeron = true;
                }
                else
                {
                    headeron = false;
                }
            }
            // HEADER TEXT BOX HANDLER
            private void tbHeaderText_TextChanged(object sender, EventArgs e)
            {
                headerstring = tbHeaderText.Text;
            }
            // SELECT FONT BUTTON
            private void button4_Click(object sender, EventArgs e)
            {
                DialogResult result;
                try
                {
                    fontDialog1.Font = headerfont;
                    result = fontDialog1.ShowDialog();
                }
                catch (Exception ex)
                {
                    eht.GeneralExceptionHandler("Invalid Font Selection", 
                                                "(01) Change Font", false, ex);
                    return;
                }
                if (result == DialogResult.OK)
                {
                    headerfont = fontDialog1.Font;
                    tbHeaderFont.Text = headerfont.FontFamily.Name.ToString() +
                    " " + headerfont.SizeInPoints.ToString() + " " + headerfont.Style.ToString();
                };
            }
    
            private void label3_Click(object sender, EventArgs e)
            {
    
            }
            // PAGE NUMBER BUTTON CHECK CHANGED HANDLER
            private void cbPageNumbersOn_CheckedChanged(object sender, EventArgs e)
            {
                if (cbPageNumbersOn.Checked)
                {
                    pagenumberson = true;
                }
                else
                {
                    pagenumberson = false;
                }
                           }
            // DATE TIME CHECKBOX HANDLER
            private void cbAddDateToHeader_CheckedChanged(object sender, EventArgs e)
            {
                if (cbAddDateToHeader.Checked)
                {
                    adddatetoheader = true;
                }
                else
                {
                    adddatetoheader = false;
                }
            }
            // ADD FILENAME CHECKBOX HANDLER
            private void cbAddFileName_CheckedChanged(object sender, EventArgs e)
            {
                if (cbAddFileName.Checked)
                {
                    addfilenametoheader = true;
                }
                else
                {
                    addfilenametoheader = false;
                }
            }
    
            // CANCEL BUTTON
            private void button2_Click(object sender, EventArgs e)
            {
                resultok = false;
                this.Close();
            }
            private bool resultok = false;
            // FORM LOAD
            private void marginsForm_Load(object sender, EventArgs e)
            {
                tbTop.Text = top.ToString("F1");
                tbBottom.Text = bottom.ToString("F1");
                tbLeft.Text = left.ToString("F1");
                tbRight.Text = right.ToString("F1");
                if (headerstring != string.Empty)
                {
                    tbHeaderText.Text = headerstring;
                }
    
                if (headeron)
                {
                    rbHeaderOn.Checked = true;
                    rbHeaderOff.Checked = false;
                }
                else
                {
                    rbHeaderOn.Checked = false;
                    rbHeaderOff.Checked = true;
                }
                if (pagenumberson)
                {
                    cbPageNumbersOn.Checked = true;
                }
                else
                {
                    cbPageNumbersOn.Checked = false;
                }
                if (adddatetoheader)
                {
                    cbAddDateToHeader.Checked = true;
                }
                else
                {
                    cbAddDateToHeader.Checked = false;
                }
                if (addfilenametoheader)
                {
                    cbAddFileName.Checked = true;
                }
                else
                {
                    cbAddFileName.Checked = false;
                }
                if (landscapemode)
                {
                    rbMarginsFormLandscape.Checked = true;
                }
                else
                {
                    rbMarginsFormPortrait.Checked = true;
                }
                tbHeaderFont.Text = headerfont.FontFamily.Name.ToString() +
                                    " " + headerfont.SizeInPoints.ToString() + " " + 
                                    headerfont.Style.ToString();
            }
        }
    }

    Image 5

  3. Find & Replace - This important editor function is not implemented by the basic RTB. It can be added by creating a separate form to handle entry of the text to search for and/or replace, with the customary function buttons for Find, Find All, Replace, Replace All and the Match Case checkbox. Since runs on top of the main RTB, it uses a delegate to control the search functions: Declare the delegate prototype in Program.cs of your forms application, or in Class.cs if building a DLL:
    C#
    // DELEGATES
    
        public delegate int Rep(string search, string replace,
               bool match, int startpos, int function);    // used for search call-back

    Then, add the actual method to perform the task to the form that contains the RichTextBox:

    C#
    // REPLACE DELEGATE FUNCTION
    
            public int ReplaceDelegateMethod(string search, string replace,
                                             bool match, int startpos, int function)
            {
                const int FIND = 1;
                const int FINDNEXT = 2;
                const int REPLACE = 3;
                const int REPLACEALL = 4;
    
                /* DEBUGMessageBox.Show("Search = "+search+"
                                         Replace = "+replace+" Match = "+match.ToString()
                    , "Delegate Test", MessageBoxButtons.OK,
                    MessageBoxIcon.Information);*/
                int currentposition = startpos;
                int stopposition = this.rtbMainForm1.Text.Length - 1;  /* text or rtf? */
                switch (function)
                {
                    case FIND:
                        {
                            this.rtbMainForm1.Find(search);
                            return (this.rtbMainForm1.SelectionStart);
                        }
                    case FINDNEXT:
                        {
                            if (search.Length == 0) // ERROR HANDLER EMPTY SEARCH FIELD
                            {
                                GeneralExceptionForm g = new GeneralExceptionForm("Find Text",
                                 "Find Field is Empty", "Error(01) - Replace Dialog", false, null);
                                g.ShowDialog();
                                g.Dispose();
                                return currentposition;
                            }
                            if (startpos < (stopposition)) // changed from stopposition-search.length
                            {
                                int searchresult = 0;
                                /*this.rtbMainForm1.SelectionStart = currentposition;*/
                                if (!match)
                                {
                                    searchresult = this.rtbMainForm1.Find(search,
                                          currentposition, stopposition, RichTextBoxFinds.None);
                                }
                                else // MATCH CASE
                                {
                                    searchresult = this.rtbMainForm1.Find(search,
                                         currentposition, stopposition, RichTextBoxFinds.MatchCase);
                                }
    
                                if (searchresult > 0)
                                {
                                    return searchresult;
                                }
                                else
                                {
                                    return 0;
                                }
                            }
                            return 0;
                        }
                    case REPLACE:
                        {
                            if (replace.Length == 0) // ERROR HANDLER EMPTY REPLACE FIELD
                            {
                                GeneralExceptionForm g = new GeneralExceptionForm("Replace Text",
                                 "Replace Field is Empty", "Error(02) - Replace Dialog", false, null);
                                g.ShowDialog();
                                g.Dispose();
                                return currentposition;
                            }
    
                            if (this.rtbMainForm1.SelectedText.Length > 0) // SKIP IF NONE SELECTED
                            {
                                this.rtbMainForm1.SelectedText = replace;
                            }
                            return currentposition;
                        }
                    case REPLACEALL:
                        {
                            if (search.Length == 0 || replace.Length == 0) // ERROR HANDLER EMPTY
                                                                           // SEARCH FIELD
                            {
                                GeneralExceptionForm g = new GeneralExceptionForm("Replace All",
                                      "Field(s) empty", "Error(03) - Replace Dialog", false, null);
                                g.ShowDialog();
                                g.Dispose();
                                return 0;
                            }
                            int searchresult = 1;
                            int count = 0;
    
                            while ((currentposition < stopposition) && 
                                                      searchresult >= 0) // changed from
                                                            // stopposition-search.length
                            {
                                if (!match)
                                {
                                    searchresult = this.rtbMainForm1.Find
                                    (search, currentposition, stopposition, RichTextBoxFinds.None);
                                }
                                else // MATCH CASE
                                {
                                    searchresult = this.rtbMainForm1.Find
                                    (search, currentposition, stopposition, RichTextBoxFinds.MatchCase);
                                }
                                if (this.rtbMainForm1.SelectedText.Length > 0)
                                {
                                    this.rtbMainForm1.SelectedText = replace;
                                    count++;
                                    currentposition = searchresult + replace.Length;
                                }
                            }
                            dlt.NotifyDialog(this, "Replaced " + 
                                             count.ToString() + " items.",displaytime);
    
                            return 1;
                        }
    
                    default:
                        {
                            return 0;
                        }
                }
            }

    Finally, call the delegate from the Search & Replace Form:

    C#
    public partial class ReplaceForm : Form
        {
            public ReplaceForm()
            {
                InitializeComponent();
            }
            // Overload with delegate - prototype def in program.cs
            // Callback Delegate for EditForm to initiate search/replace code
            public ReplaceForm(Rep r)
            {
                InitializeComponent();
                ReplaceDelegate = r; // transer a copy of the delegate to local object
            }
            public ReplaceForm(Rep r,Scr d)
            {
                InitializeComponent();
                ReplaceDelegate = r;
                ScrollDelegate = d;
            }
            public string searchstring
            {
                get
                {
                    return SearchString;
                }
                set
                {
                    SearchString = value;
                }
            }
            public string replacestring
            {
                get
                {
                    return ReplaceString;
                }
                set
                {
                    ReplaceString = value;
                }
            }
            public bool matchcase
            {
                get
                {
                    return MatchCase;
                }
                set
                {
                    MatchCase = value;
                }
            }
            private Rep ReplaceDelegate; // a private copy of the delegate in the constructor
            private Scr ScrollDelegate;
    
            private void btnReplaceFormCancel_Click(object sender, EventArgs e)
            {
                this.Close();
            }
            private string SearchString = String.Empty;
            private string ReplaceString = String.Empty;
            private bool MatchCase = false;
            private int position = 0; // CHANGED FROM 1, 01-24-2013 missed 1st word
            private const int FINDNEXT = 2;
            private const int REPLACE = 3;
            private const int REPLACEALL = 4;
            private bool foundnext = false;
    
            private void ReplaceForm_Load(object sender, EventArgs e)
            {
                if (SearchString != String.Empty)
                {
                    tbFindWhat.Text = SearchString;
                }
                if (ReplaceString != String.Empty)
                {
                    tbReplaceWith.Text = ReplaceString;
                }
                cbMatchCase.Checked = MatchCase;
            }
    
            private void btnFindNext_Click(object sender, EventArgs e)
            {
                int placeholder=0;
                SearchString = this.tbFindWhat.Text;
                placeholder = ReplaceDelegate
                       (SearchString, ReplaceString, MatchCase, position, FINDNEXT);
                ScrollDelegate();
                lblposition.Text = placeholder.ToString() + " " + SearchString;
                if (placeholder != 0)
                {
                    position = placeholder+ SearchString.Length;
                    foundnext = true;
                }
                else
                {
                    position = 0;
                    foundnext = false;
                    MessageBox.Show("Finished searching through document.",
                                    "Search Complete", MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    this.Close();
                }
            }
    
            private void tbFindWhat_TextChanged(object sender, EventArgs e)
            {
                SearchString = tbFindWhat.Text;
            }
    
            private void tbReplaceWith_TextChanged(object sender, EventArgs e)
            {
                ReplaceString = tbReplaceWith.Text;
            }
    
            private void cbMatchCase_CheckedChanged(object sender, EventArgs e)
            {
                MatchCase = cbMatchCase.Checked;
            }
    
            private void btnReplace_Click(object sender, EventArgs e)
            {
                if (!foundnext)
                {
                    btnFindNext_Click(sender, e);
                    return;  // find next word first
                }
                int placeholder = 0;
                SearchString = this.tbFindWhat.Text;
                placeholder = ReplaceDelegate
                              (SearchString, ReplaceString, MatchCase, position, REPLACE);
                lblposition.Text = placeholder.ToString() + " " + SearchString;
                if (placeholder != 0)
                {
                    position = placeholder + SearchString.Length;
                    foundnext = false;
                }
                else
                {
                    position = 0;
                    MessageBox.Show("Finished searching through document.",
                                    "Search Complete", MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    this.Close();
                }
            }
    
            private void btnReplaceAll_Click(object sender, EventArgs e)
            {
                if (ReplaceDelegate(SearchString, ReplaceString, MatchCase, 1, REPLACEALL) == 1)
                {
                    this.Close(); // RETURNS 1 if successful, 0 if field(s) are missing
                }
            }
            // SHORTCUTS FOR REPLACE FORM - You can add custom shortcuts here if desired
            private void ReplaceForm_KeyPress(object sender, KeyPressEventArgs e)
            {
                const int CTRLR = 18; // NOTE CTRL: R = 18, L =12, D = 4
                const int CTRLL = 12;
                const int CTRLD = 4;
                const int CTRLA = 1;
    
                if (System.Windows.Forms.Control.ModifierKeys.ToString() == "Control")
                {
                    int result = e.KeyChar;
                    switch (result) {
    
                        case CTRLR:
                            this.tbFindWhat.Text = "";
                            this.tbReplaceWith.Text = "right";
                            break;
                        case CTRLL:
                            this.tbFindWhat.Text = "";
                            this.tbReplaceWith.Text = "left";
                            break;
                        case CTRLD:
                            this.tbFindWhat.Text = "";
                            this.tbReplaceWith.Text = System.DateTime.Today.ToShortDateString();
                            break;
                        case CTRLA:
                            this.tbFindWhat.Text = "*";
                            break;
                        default:
                            break;
                    }
                }
            }
            //ENTER KEY EVENT HANDLERS
            private void tbFindWhat_KeyPress(object sender, KeyPressEventArgs e)
            {
                if (e.KeyChar == (char)Keys.Enter)
                {
                    e.KeyChar = (char)Keys.Tab;
                    e.Handled = true;
                    SendKeys.Send(e.KeyChar.ToString());
                }
            }
    
            private void tbReplaceWith_KeyPress(object sender, KeyPressEventArgs e)
            {
                if (e.KeyChar == (char)Keys.Enter)
                {
                    e.KeyChar = (char)Keys.Tab;
                    e.Handled = true;
                    SendKeys.Send(e.KeyChar.ToString());
                }
            }
    
            private void cbMatchCase_KeyPress(object sender, KeyPressEventArgs e)
            {
                if (e.KeyChar == (char)Keys.Enter)
                {
                    e.KeyChar = (char)Keys.Tab;
                    e.Handled = true;
                    SendKeys.Send(e.KeyChar.ToString());
                }
            }
        }
    }
  4. Proper Scrolling during Search and Replace: You will notice another delegate in the above code called ScrollDelegate(). This scrolls the selected text found by search and replace to the middle of the RTB window regardless of window size. Otherwise, the selection is always at the bottom of the window. The prototype is:
    C#
    public delegate void Scr(); // scroll delegate call-back
    

    and the method:

    C#
    // SCROLL DELEGATE FUNCTION - SCROLL SELECTION UP INTO MIDDLE OF WINDOW
            // Usage: Scrolls selected text to middle line of current window regardless of size
            // Ver: 11-20-2016
            // Credit:  http://stackoverflow.com/questions/205794/
            //          how-to-move-scroll-bar-up-by-one-line-in-c-sharp-richtextbox
    
            public void ScrollDownMethod()
            {
                int topline = rtbMainForm1.GetLineFromCharIndex
                              (rtbMainForm1.GetCharIndexFromPosition(new Point(0, 0)));
                int bottomline = rtbMainForm1.GetLineFromCharIndex
                   (rtbMainForm1.GetCharIndexFromPosition(new Point(rtbMainForm1.ClientSize.Width,
                    rtbMainForm1.ClientSize.Height)));
                int currentline = rtbMainForm1.GetLineFromCharIndex
                               (rtbMainForm1.GetFirstCharIndexOfCurrentLine());
                int middleline = topline + ((bottomline - topline) / 2);
                int linestoscroll = currentline - middleline;
                SendMessage(rtbMainForm1.Handle, (uint)0x00B6, (UIntPtr)0, (IntPtr)(linestoscroll));
                return;
    
            }

    You will also need this function from Windows:

    C#
    [DllImport("user32.dll")]
           static extern int SendMessage(IntPtr hWnd, uint wMsg, UIntPtr wParam, IntPtr lParam);
    
  5. Displaying CapsLock and Insert Key Status in the editor window: It's nice to show the status of these keys in the same manner as other editors. The solution I found, referenced below overrides the base AppIdle event handler and tracks the keystates in realtime, updating two small labels on the form containing the RichTextBox. Note that you must remove the custom handler when the program (main form) closes:
    C#
     // Base Constructor
            public EditForm()
            {
                InitializeComponent();
                DoubleBuffered = true;
                Application.Idle += App_Idle;
            }
    // Custom Application.Idle Event Handler
            // CREDIT: http://stackoverflow.com/questions/577411/
            //         how-can-i-find-the-state-of-numlock-capslock-and-scrolllock-in-net
            void App_Idle(object sender, EventArgs e)
            {
                if (System.Windows.Forms.Control.IsKeyLocked(Keys.CapsLock))
                {
                    lblCapsOn.Visible = true;
                }
                else
                {
                    lblCapsOn.Visible = false;
                }
                if ((GetKeyState(KEY_INSERT) & 1) > 0)
                {
                    lblOverStrike.Text = "OVR";
                }
                else
                {
                    lblOverStrike.Text = "INS";
                }
            }
            // FOR READING STATE OF INSERT OR CAPS LOCK KEY
    
            [DllImport("user32.dll")]
            private static extern short GetKeyState(int KeyCode);
            private const int KEY_INSERT = 0X2D;
    
            // Must Remove Override on Closing
            protected override void OnFormClosed(FormClosedEventArgs e)
            {
                Application.Idle -= App_Idle;
                base.OnFormClosed(e);
            }
  6. The Flickering Cursor Problem in Windows 10: In some settings, the way Windows handles the cursor causes it to flicker between an I-Beam and an arrow when editing in an RTB, particularly in Windows 10 with certain display settings. I encountered this when using my editor after upgrading from Windows 7, but I found this solution (originally in Visual Basic), which eliminates the problem, although a side-effect is that the cursor is now fixed as an arrow which means for example that it won't change into a hand as it should when pointing to a web address:
    C#
    // Prevent Flickering Cursor problem in Windows 10
    //CREDIT: http://www.vbforums.com/showthread.php?
    //        833547-RESOLVED-Cursor-flicker-RichTextBox-on-Windows-10-Bug
    protected override void WndProc(ref Message m)
    {
        const int WM_SETCURSOR = 0x20;
        base.WndProc(ref m);
        if (m.Msg == WM_SETCURSOR)
        {
            m.Result = (IntPtr)1;
        }
    }
    
  7. Adding Drag and Drop Features: While the RTB supports drag and drop events, the event handlers have to be added to make these work. I used the following generic classes:
    C#
    // DRAG AND DROP HANDLERS
            // 1st Step in Drag Drop
            //
            // Attach to Drag Enter Event
            //
            //
            //
            public void GenericDragEnterEventHandler
                      (object sender, System.Windows.Forms.DragEventArgs e)
            {
                if (e.Data.GetDataPresent(DataFormats.FileDrop))
                {
                    e.Effect = DragDropEffects.Move;
                }
                else
                {
                    e.Effect = DragDropEffects.None;
                }
            }
            // 2nd Step in Drag Drop
            //
            // Returns String from Drag & Drop
            //
            //
            //
            //
            public string[] GenericDragDropEventHandler
                            (object sender, System.Windows.Forms.DragEventArgs e)
            {
                if (e.Data.GetDataPresent(DataFormats.FileDrop))
                {
                    string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
                    return files;
                }
                else
                {
                    return null;
                }
            }

    Then, you can add a new instance of each to the event handlers for your RTB so the user can drag a file into the window and open that file, or insert and image into the document by dragging it to the RTB. Depending on the file type being dropped, different code is called to handle it:

    C#
    // DRAG ENTER EVENT HANDLER
            private void rtbMainForm1_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)
            {
                GenericDragEnterEventHandler(sender, e);
            }
            // DRAG DROP HANDLER
            private void rtbMainForm1_DragDrop(object sender, System.Windows.Forms.DragEventArgs e)
            {
                string ddfilename = string.Empty;
                string extension = string.Empty;
                ddfilename  = GenericDragDropEventHandler(sender, e)[0];
                if (ftt.FileExists(ddfilename ))
                {
                    extension = GetFileExtension(ddfilename);
    
                    // HANDLE IMAGE FILE INSERTION
                    if (ExtensionIsImageFile(extension))
                    {
                        InsertImage(ddfilename);
                        return;
                    }
                    // INSERT OTHER FILES
                    if (rtbMainForm1.TextLength > 0)
                    {
                        if (!dlt.QueryDialog(this, "Replace Current File?", "Open A Different File"))
                        {
                            return; // allow dialog to cancel action
                        }
                    }
                    // RTF OR TEXT
                    if (extension == "rtf" || extension == "txt" || 
                                              extension == "tex" || extension == "doc")
                    {
                        LoadText(ddfilename);
                    }
                    // OPEN OFFICE TEST FILE
                    else
                    {
                        if (extension == "odt")
                        {
                            ImportODTFile(ddfilename);
    
                            return;
                        }
                        // ALL OTHER FILE TYPES
                        else
                        {
                            ImportFile(ddfilename);
                        }
                    }
                }
            }
  8. Miscellaneous Features: Some convenience features I added include Page Up and Page Down buttons which also scroll to the top and bottom of the document when the Control Key is pressed, using this code:
    C#
    // PAGE UP
            private void btnPgUp_Click(object sender, EventArgs e)
            {
                if (ModifierKeys.HasFlag(Keys.Control))
                {
                    rtbMainForm1.SelectionStart = 0;
                    rtbMainForm1.SelectionLength = 1;
                    rtbMainForm1.ScrollToCaret();
                    return;
                }
                else
                {
                    rtbMainForm1.Focus();
                    SendKeys.Send("{PGUP}");
                }
            }
            // PAGE DOWN
            private void btnPgDn_Click(object sender, EventArgs e)
            {
                if (ModifierKeys.HasFlag(Keys.Control))
                {
                    rtbMainForm1.SelectionStart = rtbMainForm1.Text.Length;
                    rtbMainForm1.SelectionLength = 1;
                    rtbMainForm1.ScrollToCaret();
                    return;
                }
                else
                {
                    rtbMainForm1.Focus();
                    SendKeys.Send("{PGDN}");
                }
            }

    When working with some documents, it was useful to remove embedded carriage-return line-feed pairs, which appear as "\r\n" in the actual rich text markup code that underlies the RTB document displayed, so I added this function:

    C#
    // REMOVE EMBEDDED LINE BREAKS MENU ITEM
            private void removeEmbeddedCRLFsToolStripMenuItem_Click(object sender, EventArgs e)
            {
                if (rtbMainForm1.SelectedText.Length > 0)
                {
                    RemoveCRLFs();
                }
            }
            // REMOVE CRLFS - (Note: dlt.QueryDialog is a generic custom OK Cancel Dialog
            // that returns true if OK is clicked)
            private void RemoveCRLFs()
            {
                if (dlt.QueryDialog(this, "Warning: This will remove all embedded CRLFs permanently.
                                    Do You Wish to proceed?", "Remove Embedded Line Breaks")) ;
                string source = rtbMainForm1.SelectedText;
                StringBuilder sb = new StringBuilder();
                foreach (char ch in source)
                {
                    if (ch == '\r' || ch == '\n')
                    {
                        sb.Append(' ');  // remove hard coded CRLF
                        continue;
                    }
                    else
                    {
                        sb.Append(ch);
                    }
                }
                rtbMainForm1.Cut();
                Clipboard.Clear();
                Clipboard.SetData(DataFormats.Text, sb.ToString());
                rtbMainForm1.Paste();
                return;
            }

    The result of fixing these problems and omissions is the EditForm.dll class, which can be added to a project and then used by creating an instance of the editor(), customizing it as desired and invoking the method DisplayEditForm(). The .Document property contains the richtext or simple text to edit and is passed back to the caller for use in the application. The main Editor public properties are:

    C#
    // PUBLIC PROPERTY ACCESSORS
            // Allow Rich Text editing or text only
            public bool AllowRtf
            {
                get
                {
                    return _allowrtf;
                }
                set
                {
                    _allowrtf = value;
                }
    
            }
            // Allow file saving and loading from within the editor
            public bool AllowDiscAccess
            {
                get
                {
                    return _allowdiscacccess;
                }
                set
                {
                    _allowdiscacccess = value;
                }
            }
            // Offer to save the file when closing the editor
            // Disable if using only to edit for a parent application
            public bool UseSaveFileDialogWhenClosing
            {
                get
                {
                    return _EnableSaveFileDialogWhenClosing;
                }
                set
                {
                    _EnableSaveFileDialogWhenClosing = value;
                }
            }
            // The Document to edit, as RTF or Text
            public string Document
            {
                get
                {
                    return _documenttext;
                }
                set
                {
                    _documenttext = value;
                }
            }
            // Title of the editor window
            public string WindowTitle
            {
                get
                {
                    return _windowtitle;
                }
                set
                {
                    _windowtitle = value;
                }
            }
            // Open a Default file if desired
            public string FileToOpen
            {
                set
                {
                    _filetoopen = value;
                }
            }
            // Remember previous editor window size if desired
            public Size StartingWindowSize
            {
                get
                {
                    return _startsize;
                }
                set
                {
                    _startsize = value;
                }
            }

    An additional DLL, hcwgenericclasses supports the editor by providing some standardized dialogs for error handling and notifications. Here's an application example from the Demo, which creates full featured editor called qed.exe, based on EditForm.dll:

  9. Image 6

    1. C#
      using editform;          // INCLUDE IN PROJECT AND REFERENCE
      using hcwgenericclasses; // SUPPORTING LIBRARY FUNCTIONS, INCLUDE IN PROJECT AND REFERENCE
      
      //
          // QED - A Demo Stand Alone Editor using editform.dll
          // HC Williams Copyright (C) 2022 - freeware / opensource GNU public license V3
          //
          public partial class Form1 : Form
          {
              public Form1()
              {
                  InitializeComponent();
              }
      
              public Form1(string[] arguments) // if run from a command line, 
                                               // load a default file specified
              {
                  if (arguments.Length > 0)
                  {
                      file = arguments[0];
                  }
                  InitializeComponent();
              }
      
              // Prevent Low Level WINAPI errors from a recent disk removal
              // Revised: 04-10-2016
              // http://stackoverflow.com/questions/6080605/can-i-use-seterrormode-in-c-sharp-process
              // https://msdn.microsoft.com/en-us/library/
              // aa288468%28v=vs.71%29.aspx#pinvoke_callingdllexport
              // https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621%28v=vs.85%29.aspx
      
              [DllImport("kernel32.dll")]
              static extern ErrorModes SetErrorMode(ErrorModes uMode);
              [Flags]
              public enum ErrorModes : uint
              {
                  SYSTEM_DEFAULT = 0x0,
                  SEM_FAILCRITICALERRORS = 0x0001, // the one to use
                  SEM_NOALIGNMENTFAULTEXCEPT = 0x0004,
                  SEM_NOGPFAULTERRORBOX = 0x0002,
                  SEM_NOOPENFILEERRORBOX = 0x8000
              }
              private string file = String.Empty;
              private void Form1_Load(object sender, EventArgs e)
              {
                  SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS); // set on startup
                  editor ed = new editor();
                  ed.AllowDiscAccess = true;
                  ed.WindowTitle = "Quick Edit";
                  ed.UseAdvancedPrintForm = true; //Added Version 1092
                  ed.UseSpeechRecognition = true; //Added Version 1092
                  ed.UseSpellCheck = true;        //Added Version 1092
                  if (file != String.Empty)
                  {
                      ed.FileToOpen = file;
                  }
      
                  ed.DisplayEditForm(this);
                  this.Close();
              }
          }
      }

Points of Interest

The main points of interest in working on this project were those outlined above. However, there were many other small refinements that I found I had to add, such as properly sizing an image file when dropping it into the document, changing text and background color, and adding bold, underline, and italics buttons, that are documented in the EditForm.dll and demo source code.

Version 1071 adds support for Superscripts and Subscripts by adding the RTF tags "\\super" and "\\nosupersub" to the beginning and end of the selected text and reinserting the modified string back into the document source code. Also added is the ability to insert a table into the document, using a form to select the desired number of rows and columns. This is accomplished by creating a block of Rich Text markup language that encompasses the table dimensions and properties and inserting it using the clipboard. Once inserted, you can add and edit text in the table cells. Note that the table object itself cannot be edited from inside the document, although you can cut and paste it. Creating the raw table rich text code was pretty complicated. The new source code shows how it works for those who are interested.

Version 1075 adds feature to insert extended UNICODE characters from the current font into the document. This includes selected special characters and symbols. I used a ListView control in tile mode. This version disables the Import files in Hex Mode option, which was slow and didn't work very well.

VERSION 1092 02-12-2022

This version adds an improved user interface look, and option for WYSIWYG printing using my
zoomprint.dll project, instead of the more basic built in print preview, a voice dictation option
using Windows built in speech recognition (which works best if you train it), and an option for
basic spell checking using the popular NHunspell.dll.  Using this more complex version requires a couple
of extra steps when building yor project. First, the forms are designed for a high resolution display
and the Windows properties under the Compatibility tab for the your exe file may need to be tweaked by
enabling High DPI Scaling Override to "Performed by System". Second, the newer versions of NET 4.6.1 or
higher will cause an "mixed assembly" exception with NHunspell.dll unless you include a custom config file in the same directory as your application with the following contents:

XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
    </startup>
</configuration>

This text file should have the same name as your application ie "myapplication.exe.config". The same option should be enabled when using Visual Studio to build the project and run it from the IDE. This is only
a requirement if you want to use the SpellCheck feature when you create the editor. If so you must also be sure
that NHunspell.dll and its two dictionary files en_US.aff and en_US.dic are also in the application directory.

With the exception of HNunspell.dll, the other supporting dependencies hcwgenericclasses.dll, editform.dll and zoomprint.dll can be built in to your application project so they are part of the assembly and therefore don't need to be present as separate file to use it. Add them to the project, add a reference to each, and choose "Build Options -> Embedded Resource".  Then include an Auto Loader handler in your Project.cs file:

C#
static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            // EMBEDDED DLL HANDLER tested OK 01-15-2014
            // Must run in Program Class (where exception occurs
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
            Application.Run(new Form1(args));
        }
        // EMBEDDED DLL LOADER
        // VERSION 3.0 10-27-2020 derives resourcename from args and application namespace
        // assumes resource is a DLL
        // this should load any missing DLL that is properly embedded
        // This version corrects null reference exception when AssemblyStream is null
        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            string appname = Application.ProductName + "."; // gets Application Namespace
            string[] dll = args.Name.ToString().Split(','); // separates args.Name string
            string resourcename = appname + dll[0] + ".dll"; // element [0] contains the missing resource name
            Assembly MyAssembly = Assembly.GetExecutingAssembly();
            Stream AssemblyStream = MyAssembly.GetManifestResourceStream(resourcename);
            // Revised 10-27-2020
            if (AssemblyStream == null)
            {
                return null;
            }
            // end Rev
            byte[] raw = new byte[AssemblyStream.Length];
            AssemblyStream.Read(raw, 0, raw.Length);
            return Assembly.Load(raw);
        }
    }

 

This loads the dlls from the assembly when they are not found as separate files. See the example application,
qed.exe source code.

History

  • 06-13-2017 1st posting, ver 1056
  • 01-10-2018 2nd Posting ver 1071
  • 09-30-2018 3rd Posting ver 1075
  • 02-13-2022 4th Posting ver 1092
  • 12-18-2023 Added source code file for supporting genericclasses dll per request

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)