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
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:
- 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. - 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:
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:
public partial class marginsForm : Form
{
public marginsForm()
{
InitializeComponent();
}
public marginsForm(float Top,float Bottom, float Left, float Right,bool landscapemodeon)
{
InitializeComponent();
top = Top;
bottom = Bottom;
left = Left;
right = Right;
landscapemode = landscapemodeon;
}
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 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 float top, bottom, left, right = 0.0f;
private bool landscapemode = false;
private Font headerfont = new Font("Arial", 10);
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();
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();
}
private void button3_Click(object sender, EventArgs e)
{
tbTop.Text = "0.0";
tbBottom.Text = "0.0";
tbRight.Text = "0.0";
tbLeft.Text = "0.0";
}
private void btnOneInch_Click(object sender, EventArgs e)
{
tbTop.Text = "1.0";
tbBottom.Text = "1.0";
tbRight.Text = "1.0";
tbLeft.Text = "1.0";
}
private void rbHeaderOn_CheckedChanged(object sender, EventArgs e)
{
if (rbHeaderOn.Checked)
{
headeron = true;
}
else
{
headeron = false;
}
}
private void tbHeaderText_TextChanged(object sender, EventArgs e)
{
headerstring = tbHeaderText.Text;
}
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)
{
}
private void cbPageNumbersOn_CheckedChanged(object sender, EventArgs e)
{
if (cbPageNumbersOn.Checked)
{
pagenumberson = true;
}
else
{
pagenumberson = false;
}
}
private void cbAddDateToHeader_CheckedChanged(object sender, EventArgs e)
{
if (cbAddDateToHeader.Checked)
{
adddatetoheader = true;
}
else
{
adddatetoheader = false;
}
}
private void cbAddFileName_CheckedChanged(object sender, EventArgs e)
{
if (cbAddFileName.Checked)
{
addfilenametoheader = true;
}
else
{
addfilenametoheader = false;
}
}
private void button2_Click(object sender, EventArgs e)
{
resultok = false;
this.Close();
}
private bool resultok = false;
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();
}
}
}
- 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:
public delegate int Rep(string search, string replace,
bool match, int startpos, int function);
Then, add the actual method to perform the task to the form that contains the RichTextBox
:
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;
int currentposition = startpos;
int stopposition = this.rtbMainForm1.Text.Length - 1;
switch (function)
{
case FIND:
{
this.rtbMainForm1.Find(search);
return (this.rtbMainForm1.SelectionStart);
}
case FINDNEXT:
{
if (search.Length == 0)
{
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))
{
int searchresult = 0;
if (!match)
{
searchresult = this.rtbMainForm1.Find(search,
currentposition, stopposition, RichTextBoxFinds.None);
}
else
{
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)
{
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)
{
this.rtbMainForm1.SelectedText = replace;
}
return currentposition;
}
case REPLACEALL:
{
if (search.Length == 0 || replace.Length == 0)
{
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)
{
if (!match)
{
searchresult = this.rtbMainForm1.Find
(search, currentposition, stopposition, RichTextBoxFinds.None);
}
else
{
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:
public partial class ReplaceForm : Form
{
public ReplaceForm()
{
InitializeComponent();
}
public ReplaceForm(Rep r)
{
InitializeComponent();
ReplaceDelegate = r;
}
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;
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;
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;
}
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();
}
}
private void ReplaceForm_KeyPress(object sender, KeyPressEventArgs e)
{
const int CTRLR = 18;
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;
}
}
}
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());
}
}
}
}
- 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:
public delegate void Scr();
and the method:
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:
[DllImport("user32.dll")]
static extern int SendMessage(IntPtr hWnd, uint wMsg, UIntPtr wParam, IntPtr lParam);
- 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:
public EditForm()
{
InitializeComponent();
DoubleBuffered = true;
Application.Idle += App_Idle;
}
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";
}
}
[DllImport("user32.dll")]
private static extern short GetKeyState(int KeyCode);
private const int KEY_INSERT = 0X2D;
protected override void OnFormClosed(FormClosedEventArgs e)
{
Application.Idle -= App_Idle;
base.OnFormClosed(e);
}
- 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:
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;
}
}
- 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:
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;
}
}
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:
private void rtbMainForm1_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)
{
GenericDragEnterEventHandler(sender, e);
}
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);
if (ExtensionIsImageFile(extension))
{
InsertImage(ddfilename);
return;
}
if (rtbMainForm1.TextLength > 0)
{
if (!dlt.QueryDialog(this, "Replace Current File?", "Open A Different File"))
{
return;
}
}
if (extension == "rtf" || extension == "txt" ||
extension == "tex" || extension == "doc")
{
LoadText(ddfilename);
}
else
{
if (extension == "odt")
{
ImportODTFile(ddfilename);
return;
}
else
{
ImportFile(ddfilename);
}
}
}
}
- 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:
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}");
}
}
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:
private void removeEmbeddedCRLFsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (rtbMainForm1.SelectedText.Length > 0)
{
RemoveCRLFs();
}
}
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(' ');
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:
public bool AllowRtf
{
get
{
return _allowrtf;
}
set
{
_allowrtf = value;
}
}
public bool AllowDiscAccess
{
get
{
return _allowdiscacccess;
}
set
{
_allowdiscacccess = value;
}
}
public bool UseSaveFileDialogWhenClosing
{
get
{
return _EnableSaveFileDialogWhenClosing;
}
set
{
_EnableSaveFileDialogWhenClosing = value;
}
}
public string Document
{
get
{
return _documenttext;
}
set
{
_documenttext = value;
}
}
public string WindowTitle
{
get
{
return _windowtitle;
}
set
{
_windowtitle = value;
}
}
public string FileToOpen
{
set
{
_filetoopen = value;
}
}
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:
-
-
using editform;
using hcwgenericclasses;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
public Form1(string[] arguments)
{
if (arguments.Length > 0)
{
file = arguments[0];
}
InitializeComponent();
}
[DllImport("kernel32.dll")]
static extern ErrorModes SetErrorMode(ErrorModes uMode);
[Flags]
public enum ErrorModes : uint
{
SYSTEM_DEFAULT = 0x0,
SEM_FAILCRITICALERRORS = 0x0001,
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);
editor ed = new editor();
ed.AllowDiscAccess = true;
ed.WindowTitle = "Quick Edit";
ed.UseAdvancedPrintForm = true;
ed.UseSpeechRecognition = true;
ed.UseSpellCheck = true;
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:
="1.0"="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:
static class Program
{
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
Application.Run(new Form1(args));
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
string appname = Application.ProductName + ".";
string[] dll = args.Name.ToString().Split(',');
string resourcename = appname + dll[0] + ".dll";
Assembly MyAssembly = Assembly.GetExecutingAssembly();
Stream AssemblyStream = MyAssembly.GetManifestResourceStream(resourcename);
if (AssemblyStream == null)
{
return null;
}
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