基于 RichTextBox 的全功能编辑器





5.00/5 (22投票s)
为 C# 应用程序创建可重用的编辑器,
引言
NET Framework中的RichTextBox
控件(RTB)提供了一种以所见即所得的方式编辑RTF文件和旧版MS Word文档中使用的richtext
标记代码以及纯文本的简单方法。然而,在其基本形式中,它缺乏更完整编辑器所需的功能,例如查找和替换、文档打印、页面布局以及文件和图像的拖放。
Using the Code
该项目最初是为了将基础RichTextBox
类打造成一个更完整的编辑器,可以用作我正在开发的其他应用程序的可重用组件。虽然RTB有许多有用的属性和方法,但它在基本形式中并非一个功能齐全的编辑器。在添加我認為必要的改进的过程中,我遇到了以下主要问题,以及从网络上众多不同来源找到的解决方案。
- 打印 - RTB没有内置的打印内容的方法。我创建了一个名为
PrintHelper
的类,其中包含一个GeneralPrintForm
构造函数,该构造函数在CodeProject上作为单独的“技巧与窍门”帖子提供:https://codeproject.org.cn/Tips/829368/A-Simple-RTF-Print-Form”。请注意,Net的PrintDialog
还提供一个PrintPreview
窗口,显示文档的打印效果。PrintHelper
类使用默认的打印和预览对话框。 - 页面布局和边距 - RTB在窗体上显示时,其内容缺乏可见边距,这是一个相对简单的问题。解决此问题的简单方法是将RTB包含在一个稍微大一点的Panel容器中,并设置
FixedSingle BorderStyle
,这样在滚动文本控件和面板的周围边缘之间就会出现一个明显的边距。请注意,这只是模拟了打印页面的边距。要设置打印页面的实际边距、页眉和页脚,可以使用上面第1项中PrintHelper
类中的GeneralPrintForm
,将System.Drawing.Printing.PageSettings
对象填充好并通过其构造函数发送到GeneralPrintForm
。// 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); }
我创建了一个简单的窗体,用于在编辑器内向用户收集这些设置。
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(); } } }
- 查找与替换 - 这个重要的编辑器功能不是由基本RTB实现的。可以通过创建一个单独的窗体来添加此功能,该窗体负责处理要搜索和/或替换的文本的输入,以及传统的按钮,如查找、全部查找、替换、全部替换和“区分大小写”复选框。由于它运行在主RTB之上,它使用委托来控制搜索功能:在窗体应用程序的Program.cs中,或者如果构建DLL,则在Class.cs中声明委托原型。
// DELEGATES public delegate int Rep(string search, string replace, bool match, int startpos, int function); // used for search call-back
然后,将执行任务的实际方法添加到包含
RichTextBox
的窗体中。// 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; } } }
最后,从“查找和替换”窗体调用
delegate
。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()); } } } }
- 搜索和替换过程中的正确滚动:您还会注意到上面代码中另一个名为
ScrollDelegate()
的delegate
。无论窗口大小如何,它都能将通过搜索和替换找到的选定文本滚动到RTB窗口的中间。否则,选区将始终位于窗口底部。原型是public delegate void Scr(); // scroll delegate call-back
以及方法
// 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; }
您还将需要来自Windows的这个函数。
[DllImport("user32.dll")] static extern int SendMessage(IntPtr hWnd, uint wMsg, UIntPtr wParam, IntPtr lParam);
- 在编辑器窗口中显示大写锁定和插入键状态:很高兴能像其他编辑器一样显示这些键的状态。我找到的解决方案(如下所述)会覆盖基类的
AppIdle
事件处理程序,并实时跟踪按键状态,更新包含RichTextBox
的窗体上的两个小标签。请注意,当程序(主窗体)关闭时,必须删除自定义处理程序。// 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); }
- Windows 10 中的光标闪烁问题:在某些设置下,Windows处理光标的方式会导致在RTB编辑过程中,光标在I型光标和箭头之间闪烁,特别是在Windows 10中以及某些显示设置下。在我从Windows 7升级后使用我的编辑器时遇到了这个问题,但我找到了这个解决方案(最初是用Visual Basic实现的),它消除了这个问题,尽管一个副作用是光标现在固定为箭头,这意味着例如,它不会像指向网页地址时那样变成手形。
// 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; } }
- 添加拖放功能:虽然RTB支持拖放事件,但必须添加事件处理程序才能使其工作。我使用了以下通用类。
// 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; } }
然后,您可以将每个类的实例添加到RTB的事件处理程序中,以便用户可以将文件拖放到窗口中并打开该文件,或者通过将其拖放到RTB上来将图像插入文档。根据被拖放的文件类型,会调用不同的代码来处理它。
// 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); } } } }
- 杂项功能:我添加的一些便捷功能包括“上一页”和“下一页”按钮,当按下Control键时,它们还会滚动到文档的顶部和底部,使用此代码。
// 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}"); } }
在处理某些文档时,移除嵌入的回车换行符对很有用,这些符号在RTB文档显示所依据的实际富文本标记代码中显示为“
\r\n
”,因此我添加了这个函数。// 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; }
修复这些问题和遗漏的结果是EditForm.dll类,可以将其添加到项目中,然后通过创建
editor()
的实例来使用它,根据需要进行自定义,并调用DisplayEditForm()
方法。.Document
属性包含要编辑的richtext
或纯文本,并将其传回调用者以供应用程序使用。主要的编辑器public
属性是:// 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; } }
另一个DLL,
hcwgenericclasses
,通过提供一些标准化的对话框用于错误处理和通知来支持此编辑器。以下是演示中的一个应用程序示例,它创建了一个基于EditForm.dll的完整功能编辑器,名为qed.exe。 -
-
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(); } } }
-
关注点
在处理此项目时,主要值得关注的点如上所述。但是,我还发现需要添加许多其他小的改进,例如在拖放到文档时正确调整图像文件大小、更改文本和背景颜色,以及添加粗体、下划线和斜体按钮,这些都在EditForm.dll和演示源代码中有记录。
版本1071通过在选定文本的开头和结尾添加RTF标签“\\super
”和“\\nosupersub
”并重新将修改后的string
插入文档源代码,增加了对上标和下标的支持。还增加了使用窗体选择所需行数和列数来插入表格的功能。这是通过创建一个包含表格尺寸和属性的富文本标记语言块,并使用剪贴板插入来实现的。插入后,您可以向表格单元格中添加和编辑文本。请注意,表格对象本身无法在文档内部进行编辑,尽管您可以剪切和粘贴它。创建原始表格富文本代码非常复杂。新的源代码展示了它的工作原理,供感兴趣的人参考。
版本1075增加了从当前字体中插入扩展的UNICODE字符到文档的功能。这包括选定的特殊字符和符号。我使用了平铺模式的ListView
控件。此版本禁用了十六进制模式下的导入文件选项,该选项速度很慢且效果不佳。
版本 1092 2022年2月12日
此版本增加了改进的用户界面外观,以及使用我的
zoomprint.dll 项目进行所见即所得打印的选项,而不是更基本的内置打印预览,以及语音听写选项
使用Windows内置的语音识别(如果经过训练效果最佳),以及一个选项用于
使用流行的NHunspell.dll进行基本拼写检查。 使用这个更复杂的版本在构建项目时需要几个
额外的步骤。首先,窗体设计用于高分辨率显示器
并且您exe文件的兼容性选项卡下的Windows属性可能需要通过
启用“由系统执行”的高DPI缩放覆盖来调整。其次,NET 4.6.1或更高版本
更高版本将与NHunspell.dll一起导致“混合程序集”异常,除非您在应用程序的同一目录下包含一个自定义配置文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
</startup>
</configuration>
这个文本文件应该与您的应用程序同名,例如“myapplication.exe.config”。当使用Visual Studio构建项目并从IDE运行它时,也应该启用相同的选项。这仅仅是
如果您想在创建编辑器时使用拼写检查功能,则需要满足此要求。如果是这样,您还必须确保
NHunspell.dll及其两个字典文件en_US.aff和en_US.dic也在应用程序目录中。
除了HNunspell.dll之外,其他支持的依赖项hcwgenericclasses.dll、editform.dll和zoomprint.dll可以嵌入到您的应用程序项目中,成为程序集的一部分,因此无需单独的文件即可使用它们。将它们添加到项目中,添加对每个库的引用,然后选择“构建选项->嵌入式资源”。 然后在您的Project.cs文件中包含一个自动加载程序处理程序。
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);
}
}
当找不到dll作为单独文件时,这会将dll从程序集中加载。
qed.exe源代码。
历史
- 2017年6月13日 首次发布,版本 1056
- 2018年1月10日 第二次发布 版本 1071
- 2018年9月30日 第三次发布 版本 1075
- 2022年2月13日 第四次发布 版本 1092
- 2023年12月18日 根据要求添加了支持通用类dll的源代码文件