第 2 部分:CodeProject.Show 深度解析 - 探索其背后的源代码





5.00/5 (3投票s)
深入探索 CodeProject 离线文章编辑器 CodeProject.Show 背后的源代码。
本文是基于以下文章的后续文章:第 1 部分:CodeProject.Show - CodeProject 离线文章编辑器 。该链接是源代码更新和如何使用此应用的官方页面。请在此处下载。 **本文将深入探讨源代码,没有图片,只有源代码。**
引言
本文旨在讨论 CodeProject.Show(一款 CodeProject 离线文章编辑器)背后的源代码。由于我们美丽国家(原文此处可能是作者所在地区)的网络连接问题,我考虑这个想法已经有一段时间了。我想要一个离线文章编辑器,能够复制 CodeProject 在线编辑器的相同功能。我希望它易于使用,并且能够在一个地方撰写和存储所有文章。我开始喜欢在命名我的应用时使用“.Show”前缀。我想我是受到了 Vertigo 家族树应用 Family.Show 的启发。只可惜它不能联网工作。因此,我尝试使用 JQuery Mobile 和 D3 来复制一个家族树,如这篇文章和这篇文章中所述。
长话短说,我启动了 Visual Studio,创建了一个新项目,并因此命名为 CodeProject.Show。我需要一个树视图来列出我所有的文章,一个所见即所得编辑器,以及一个 HTML 编辑器。我决定我的后端将使用 SQLite,尽管它在文章中并不常用,后来我决定文章将存储在 CodeProject.Show 应用程序目录下的 Articles 文件夹中的单个 HTML 文件中。
一些显而易见的假设:您之前已经撰写过 CodeProject 在线文章,熟悉文字处理,并且阅读了我讨论使用此 CodeProject.Show 文章。我们将在此处查看使该应用运行的源代码。您也熟悉创建 Visual Studio VB.Net 应用程序。如果您懂 C#,也可以使用 SharpDevelop 来转换项目。
创建 VB.Net 项目
- 我在我的 VB.Project 中创建了一个窗体,并将其设置为在启动时居中和最大化,通过 Project > Add Windows Form。
- 我添加了 StatusBar、ToolStrip、ImageList、FontDialog、ColorDialog 和 Timer 控件。
- 从工具箱的容器中,我双击了 Split Containers。
- 我将另一个 Split container 拖放到现有面板的 Panel 2 上。
- 我将一个 TreeView 拖放到第一个拆分容器的第一个面板上。这在 Common Controls 下。
- 我将一个 WebBrowser 控件拖放到第二个拆分容器的 panel1 上。
- 然后我转到 Tools > Extensions and Updates,搜索 ICSharp.TextEditor 并将其安装到我的项目中。将其设置在工具箱中,然后将其拖放到第二个拆分容器的 panel2 上。
- 我确保这三个控件的 Dock 位置都设置为 FullDock。
- 我添加了对 SQLite、SQLServerCe、mshtml 和 Speech 库的引用。
- 我创建了工具栏按钮。
准备我的 CodeProject 模板
我从 CodeProject 下载了 提交模板文件,并对其进行了一些更改。这些更改是:
- 删除正文部分的步骤 3。
- 在标题中添加了此 CSS,以处理带有灰色背景的引用外观。
.quote {padding:0 10px 10px 27px;margin-left:.25em;color:#565;margin-right:1em;margin-bottom:1em;background:url("quote.gif") no-repeat scroll left top #eee}
- 在正文中添加了此内容(以确保鼠标指针在 webbrowser 控件上显示,以便在线编辑)。
contentEditable='true'
我将模板命名为 **article.txt**,将其复制到我的 VB 项目位置,并设置为在编译时始终复制。以后代码会引用它。
准备我的 article.db,一个 SQLite 数据库
我想要记录我的文章,每篇文章都有一个唯一的编号。我启动了 Database.NET,这是一个免费的数据库管理软件。我喜欢这个应用的便捷性。
- 使用 File > Connect > SQLite > Create,我直接在我的项目位置创建了一个新数据库。
- 之后,我选择 Tables > Right Click > Create Table > 然后按 **+** 添加列。我添加了 ID(整数,主键,自增)、article(文章名称)等。
- 点击 Save,输入表名,我称之为 articles。
代码助手
我之前写了一些类来帮助我管理应用程序。它们是 _Files_(用于任何与文件操作相关的内容)、_SQLite_(用于任何与 SQLite 相关的内容)、_Map_(数据字典助手)、_Speech_(任何与语音相关的内容)、_clsWinForms_(用于 WinForms 应用,但现在已调整为 _Common_)。我将它们复制到这个项目中以供使用。我很快就会解释 CodeProject.Show 使用到的功能。
我现在已准备好创建界面并编写代码了。
背景
开发这样的应用程序需要一些研究。Microsoft 有一个 webcontrol,可以使用 ExecCommand 向其传递命令。有关这些命令的更多信息可以在这里找到。由于这是该项目在 WinForms 应用程序中的第一个版本,我必须说,创建一个这样的文章编辑器的一个灵感来自于这篇文章。考虑到这一点,在搜索了类似文章之后,我有一天决定就做这个,现在它就在这里了。我必须承认,我以前从未如此详细地编程过一个 webcontrol,并且感谢通过 Google,我能够将我学到的关于 webcontrol 及其使用 ExecCommand 进行自动化的绝大部分内容整合到这个应用程序中。对我来说,这一切中最棒的部分是:我知道如何去做,并且我“改变世界”的激情再次实现了。
您作为该应用的潜在最终用户,欢迎提出建议和意见。我非常需要它们,即使只是关于增强这个应用程序,因为它能为人们的生活增添很多价值,就像它为我带来了价值一样。您现在正在阅读的文章,概念化和创建都直接来自 CodeProject.Show。
正如我在上面的链接中提到的,使用 ExecCommand 和 webcontrol,您可以实现很多功能,但有些命令在大多数浏览器中都不支持,即使是 Internet Explorer。因此,我不得不编写一些代码来使某些功能正常工作。例如,将一段文章文本包装在 `` 元素中,我不得不写如下脚本:
Private Sub WrapSelection(elementX As String)
' this does the same as formatcode, when it does not work
' element to set the attribute
Dim hElement As IHTMLElement
' get the document
Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
' get selected range
Dim range As IHTMLTxtRange = doc.selection.createRange()
' text block quote
hElement = doc.createElement(elementX)
hElement.innerHTML = range.htmlText
range.pasteHTML(hElement.outerHTML)
End Sub
其中 **elementX** 可以是任何东西,例如“code”、“div”等。
要使用 CodeProject.Show,需要了解其结构。该应用程序分为几个部分。它们是:
工具栏 - 它提供了撰写和格式化文章的大部分功能。
树视图 - 它列出了您所有的文章,您可以单击一篇文章打开它、删除它、重命名它等。它是可切换的,您可以隐藏和显示它。
文章编辑器 - 屏幕的中间部分,您可以在其中撰写文章。这部分使用处于设计模式且 contentEditable 为 on 的 WebControl。您的文章每 1 分钟保存一次,并在您撰写时进行备份。每篇文章都以唯一的文章编号保存为 html 文件。
HTML - 屏幕的右侧部分也是可切换的,您可以在其中查看文章的 HTML 源代码。此部分为只读。
状态栏 - CodeProject.Show 的底部是状态栏,它提供了有关您文档的统计信息。它显示文章中有多少词,最后保存时间,包含文章详细信息的文件夹大小及其位置。
本文的目的是关于我如何创建这个应用程序。我现在将简要介绍各部分的源代码。
使用代码
1. 使用 CodeProject.Show 管理文章
使用 CodeProject.Show 管理文章包括:创建新文章、删除文章、重命名文章、重置文章以及将文章保存为 html 文件。
1.1 文件 > 新建
这是将新文章添加到您的文章数据库。您将被要求覆盖树视图项,输入新文章名称,然后按 Enter。
Private Sub cmdNewArticle_Click(sender As Object, e As EventArgs) Handles cmdNewArticle.Click
' add an article to the treeview
Timer1.Enabled = False
Dim pNode As TreeNode = treeArticles.Nodes("root")
SelectedArticle = pNode.Nodes.Add("new", "New Article", "page", "page")
' ensure the treenode is visible
SelectedArticle.EnsureVisible()
' ensure the treenode is the selected node for editing
treeArticles.SelectedNode = SelectedArticle
' set the editing mode to true
treeArticles.LabelEdit = True
' begin the edit of the node
If Not SelectedArticle.IsEditing Then
SelectedArticle.BeginEdit()
End If
Timer1.Enabled = True
End Sub
有一个计时器用于每分钟保存一次文章。要添加新文章,我们首先通过 timer1.enabled = false 关闭计时器。当应用程序启动时,它会在根节点下加载树视图中所有可用的文章。然后将所有子文章添加到该根节点。每篇新文章的名称将是“New Article”,并显示一个名为“page”的图标,该图标来自链接到树视图的 ImageList。此过程然后触发树视图的 **LabelEdit** 和 **BeginEdit**,以使用户能够覆盖文章的名称。
文章名称长度为 **255** 个字符,并且不接受特殊字符,如 ,?><|* 等,这与文件名相同。
一旦用户在输入文章名称后按 Enter,树视图就会触发 **AfterLabelEdit** 事件。
Private Sub treeArticles_AfterLabelEdit(sender As Object, e As NodeLabelEditEventArgs) Handles treeArticles.AfterLabelEdit
' after the label is edited, add the article name to the database
If Not (e.Label Is Nothing) Then
If e.Label.Length > 0 Then
If e.Label.IndexOfAny(New Char() {"@"c, "."c, ","c, "!"c, "\"c, ":"c, "*"c, "?"c, "<"c, ">"c, "|"c, "/"c}) = -1 Then
' Stop editing without canceling the label change.
e.Node.EndEdit(False)
' get new article name and add it to the database
articleTitle = e.Label
'ensure the article is 255 characters long
articleTitle = Strings.Left(articleTitle, 255).Trim
articleKey = e.Node.Name
articlePrefix = Common.MvField(articleKey, 1, "-")
articleID = Common.MvField(articleKey, 2, "-")
Select Case articlePrefix
Case "new"
' we are adding a new article
' insert a new article to the database
Dim article As New Map
article.Put("article", articleTitle)
SQLite.InsertMap("articles", article)
' get the id of the article from the database
articleID = SQLite.RecordReadToMv("articles", "article", articleTitle, "id")
PrepareArticle(e.Node, articleID, articleTitle)
' display the article
ReadArticle(articleID)
Exit Sub
Case "article"
' we are updating an existing article, change the article title
articleKey = e.Node.Name
' get the article id
articleID = Common.MvField(articleKey, 2, "-")
' update a new article to the database
Dim orticle As New Map
orticle.Put("article", articleTitle)
Dim warticle As New Map
warticle.Put("ID", articleID)
SQLite.UpdateMap("articles", orticle, warticle)
ReadArticle(articleID)
Exit Sub
End Select
Else
' Cancel the label edit action, inform the user, and
' place the node in edit mode again.
e.CancelEdit = True
MessageBox.Show("Invalid tree node label." & _
Microsoft.VisualBasic.ControlChars.Cr & _
"The invalid characters are: '@', '.', ',', '!', '?', '>', '<', '|', '*', ':', '\', '/'", _
"Article Edit", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
e.Node.BeginEdit()
Exit Sub
End If
Else
' Cancel the label edit action, inform the user, and
' place the node in edit mode again.
e.CancelEdit = True
MessageBox.Show("Invalid tree node label." & _
Microsoft.VisualBasic.ControlChars.Cr & _
"The label cannot be blank", "Article Edit", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
e.Node.BeginEdit()
Exit Sub
End If
End If
End Sub
这会检查文章名称是否符合特殊字符的要求,将其限制为 255 个字符,然后更新数据库的 articles 表并创建文章的文件夹。每篇树视图的文章名称前面会加上“article-”前缀,然后是读取自 SQLite 表的文章编号。MvField 类似于 split 函数,用于从分隔的字符串中获取一个项。每篇新文章的 article key/name 都是 new,因此这里我们还检查是添加新文章还是重命名现有文章。如果文章是新的,我们读取文章名称并将其添加到数据库,获取分配的唯一自增编号,准备并读取文章。准备文章确保文章的所有内容都已就绪。
Sub PrepareArticle(newNode As TreeNode, ID As String, Title As String)
' now add the images and links
Dim zNode As TreeNode = newNode.Nodes.Add("zips-" & ID, "Zip Files", "zip", "zip")
Dim iNode As TreeNode = newNode.Nodes.Add("images-" & ID, "Media", "camera", "camera")
Dim lNode As TreeNode = newNode.Nodes.Add("links-" & ID, "Links", "link", "link")
' create respective links to project
articlePath = articlesPath & "\" & ID
articleBaK = articlePath & "\BAK"
Files.Dir_Create(articlePath)
Files.Dir_Create(articleBaK)
Files.File_CopyToFolder(Common.AppPath & "\quote.gif", articlePath)
articleFile = articlePath & "\" & ID & ".html"
' if the file does not exist, create a blank one
If Files.File_Exists(articleFile) = False Then
' get the blank article contents
articleContent = Files.File_Data(blankArticle)
' save the new blank article
Files.File_Update(articleFile, articleContent)
End If
End Sub
对于每篇文章,都会创建三个子节点:Zip Files(用于存放您的源代码)、Media(用于存放所有图片等)和 Links(用于存放文章创建过程中创建的所有链接)。在您的安装文件夹中会使用文章 ID 创建一个文章路径,例如 **C:\CPS\Articles\1\1.html**。此文件夹将存储与文章相关的所有内容。还有一个名为 BAK 的备份文件夹,用于存储文章的每分钟间隔版本。这确保您永远不会真正丢失一篇文章。这是我想要避免的事情,但是由于这种每分钟保存一次的机制,您可能希望在完成文章后清理它们。上述方法还会检查文章文件是否存在,如果不存在,则将 blankArticle(即 article.txt)的内容复制到文章文件中。
管理文件
读取文件内容
Public Shared Function File_Data(ByVal StrPath As String) As String
If File.Exists(StrPath) = True Then
Dim readText As String = File.ReadAllText(StrPath)
Return readText
Else
Return ""
End If
End Function
写入文件内容
Public Shared Function File_Update(ByVal strFileName As String, ByVal strContent As String, Optional ByVal boolAppend As Boolean = False) As Boolean
Try
Dim strfolder As String = Files.File_Token(strFileName, FileTokenType.Path, "\", False)
If Not Files.Dir_Exists(strfolder) Then
Files.Dir_Create(strfolder)
End If
My.Computer.FileSystem.WriteAllText(strFileName, (strContent & ChrW(13) & ChrW(10)), boolAppend)
Return True
Catch exc As Exception
MsgBox(strFileName & vbCr & vbCr & "The file could not be saved! Please try again!", MsgBoxStyle.Critical, "File Save Error")
Return False
End Try
End Function
您可以在此处指定完整的文件路径,它将递归创建文件夹以存储文件。File_Token 返回一个特定的文件令牌,它可以是路径、扩展名等。
文件大小
Public Shared Function File_SizeName(ByVal Bytes As Long) As String
If (Bytes >= &H40000000) Then
Return (Strings.Format((((CDbl(Bytes) / 1024) / 1024) / 1024), "#0.00") & " GB")
End If
If (Bytes >= &H100000) Then
Return (Strings.Format(((CDbl(Bytes) / 1024) / 1024), "#0.00") & " MB")
End If
If (Bytes >= &H400) Then
Return (Strings.Format((CDbl(Bytes) / 1024), "#0.00") & " KB")
End If
If ((Bytes > 0) And (Bytes < &H400)) Then
Return (Conversions.ToString(Conversion.Fix(Bytes)) & " Bytes")
End If
Return "0 Bytes"
End Function
这是为了状态栏的文件大小,我们读取文件的长度并显示其大小以及各种指示符。
读取文章
Sub ReadArticle(ID As String)
Common.HourGlassShow(Me)
' create respective links to project
articlePath = articlesPath & "\" & ID
articleBaK = articlePath & "\BAK"
' define the article path
articleFile = articlePath & "\" & ID & ".html"
If Files.File_Exists(articleFile) = True Then
' get the file contents
articleContent = Files.File_Data(articleFile)
' load the file to the web browser
txtContent.DocumentText = articleContent
' count the words in the article
articleWords = CountWords(articleContent)
' update the status bar
StatusMessage(StatusBar, "Words: " & articleWords & " |", 2)
' once a document has been loaded, set to design mode
txtContent.ActiveXInstance.document.designmode = "On"
' set the tag, we will use this to save the article
txtContent.Tag = articleFile
' get links for this article
GetArticleLinks(ID)
GetArticleMedia(ID)
'Timer1.Enabled = True
Dim fsize As Long = Files.Dir_Size(articlePath)
Dim fsize1 As String = Files.File_SizeName(fsize)
StatusMessage(StatusBar, "Location: " & txtContent.Tag & " |", 5)
StatusMessage(StatusBar, "Size: " & fsize1 & "|", 4)
StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(articleFile) & "|", 3)
SetHTML(articleContent)
Else
SetHTML("")
StatusMessage(StatusBar, "Words: 0 |", 2)
StatusMessage(StatusBar, "Location: |", 5)
StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(articleFile) & "|", 3)
StatusMessage(StatusBar, "Size: |", 4)
End If
Timer1.Enabled = True
Speech.StopSpeaking()
HourGlassHide(Me)
End Sub
此方法基本上执行几项操作。文章 ID 从节点名称读取,例如 article-1,然后返回。接着生成文章路径,并检查文章文件是否存在于计算机上。如果存在,则读取文件内容,并将这些内容显示在 webbrowser 中(txtContent)。统计文章中的字数,然后打开 webbrowser 的 designMode 以启用 webbrowser 编辑。文章的路径被保存在 webbrowser 的 tag 属性中(这用于在间隔期间保存文章)。
之后,文章中存在的所有链接(包括媒体)都会被读取,并加载到每个文章各自的节点中。文件夹内容的尺寸被计算,状态栏被更新。如果曾读取文章文本,则停止读取。
Private Sub GetArticleLinks(ID As String)
Common.HourGlassShow(Me)
' find all nodes with this key
Dim bFound As Boolean = False
Dim lnkNode As TreeNode = Nothing
Dim arr As TreeNode() = treeArticles.Nodes.Find("links-" & ID, True)
For i As Integer = 0 To arr.Length - 1
lnkNode = arr(i)
bFound = True
Exit For
Next
If bFound = True Then
' remove all listed links
lnkNode.Nodes.Clear()
Dim lnkKey As String
Dim lnkPos As Integer = 0
Dim eletarget As String
For Each ele As HtmlElement In txtContent.Document.Links
lnkPos = lnkPos + 1
lnkKey = "link-" & ID & "-" & lnkPos
eletarget = ele.GetAttribute("href")
lnkNode.Nodes.Add(lnkKey, eletarget, "link", "link")
Application.DoEvents()
Next
End If
Common.HourGlassHide(Me)
End Sub
此处每个文章的 Links 节点会被清空并重新加载任何现有的链接。这发生在用户选择文章的 Links 节点时。在此过程中会显示沙漏图标。
Private Sub GetArticleMedia(ID As String)
HourGlassShow(Me)
' find all nodes with this key
Dim bFound As Boolean = False
Dim lnkNode As TreeNode = Nothing
Dim arr As TreeNode() = treeArticles.Nodes.Find("images-" & ID, True)
For i As Integer = 0 To arr.Length - 1
lnkNode = arr(i)
bFound = True
Exit For
Next
If bFound = True Then
' remove all listed links
lnkNode.Nodes.Clear()
Dim imgsrc As String
Dim lnkKey As String
Dim lnkPos As Integer = 0
Dim cleanLnk As String
Dim fName As String
' put border on images
Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
For Each image As HTMLImg In doc.images
If image IsNot Nothing Then
imgsrc = image.src
imgsrc = imgsrc.Replace("about:", "")
lnkPos = lnkPos + 1
lnkKey = "media-" & ID & "-" & lnkPos
' copy the images to the article folder
If InStr(imgsrc, "file:///") > 0 Then
cleanLnk = imgsrc.Replace("file:///", "")
cleanLnk = cleanLnk.Replace("/", "\")
articlePath = articlesPath & "\" & ID
articleBaK = articlePath & "\BAK"
Files.File_CopyToFolder(cleanLnk, articlePath)
' get file name and update media link
fName = Files.File_Token(cleanLnk, Files.FileTokenType.FileName)
' try and update image link
image.src = fName
lnkNode.Nodes.Add(lnkKey, fName, "camera", "camera")
Else
lnkNode.Nodes.Add(lnkKey, imgsrc, "camera", "camera")
End If
End If
Application.DoEvents()
Next
End If
HourGlassHide(Me)
End Sub
Media 节点会加载文档 DOM 中所有可用的图像、视频等。图像源将基本上是每次通过工具栏插入图像时指向完整文件路径的链接。对于 CodeProject,所有图像链接都应该在文件夹级别是绝对路径,因此必须删除完整路径。此方法在扫描所有可用链接时,会检查链接是否是完整文件路径,然后进行清理,并将媒体文件复制到文章文件夹。
所有这些链接和媒体的读取都发生在文章在树视图中被点击时,让我们看看下面会发生什么。
Private Sub treeArticles_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles treeArticles.NodeMouseClick
' get the selected article
Dim imgLink As String
SelectedArticle = e.Node
If TypeName(SelectedArticle) <> "Nothing" Then
' display the tag contents on the html part of the screen
articleKey = SelectedArticle.Name
articleID = Common.MvField(articleKey, 2, "-")
articlePrefix = Common.MvField(articleKey, 1, "-")
Select Case articlePrefix
Case "article"
' update title of page
Me.Text = "CodeProject.Show: " & SelectedArticle.Text
' read the article
ReadArticle(articleID)
Case "images"
Me.GetArticleMedia(articleID)
SaveContent()
Case "links"
Me.GetArticleLinks(articleID)
Case "zips"
Case "articles"
SetHTML("")
txtContent.DocumentText = ""
Case "link"
' open the link in the explorer
txtContent.Navigate(SelectedArticle.Text)
Case "media"
' open the media in the explorer
imgLink = articlesPath & "\" & articleID & "\" & SelectedArticle.Text
txtContent.Url = New Uri(imgLink)
End Select
End If
End Sub
选定的节点存储在 SelectedArticle 中以供引用。文章 ID 和其他信息被读取,并根据前缀进行处理:
- article - 读取文章详情。
- images - 加载文章的所有图像/媒体。
- links - 加载文章的所有链接。
- link - 在编写区域的 webbrowser 控件中打开它。
- media - 在编写区域的 webbrowser 控件中打开它。
1.2 文件 > 删除
此选项用于删除您的文章。您必须从列表中选择您的文章,然后点击 File > Delete 将其删除。已删除的文章无法撤销。
Private Sub cmdDeleteArticle_Click(sender As Object, e As EventArgs) Handles cmdDeleteArticle.Click
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
Common.MyMsgBox("You need to select an article to delete first!", , , "Delete Article")
Else
Timer1.Enabled = False
articleTitle = SelectedArticle.Text
articleKey = SelectedArticle.Name
articleID = Common.MvField(articleKey, 2, "-")
Dim ans As MsgBoxResult = Common.MyMsgBox("Delete: " & articleTitle & vbCrLf & vbCrLf & _
"Are you sure that you want to delete this article, you will not be able to undo your changes. Continue?", _
"yn", "q", "Confirm Delete")
If ans = MsgBoxResult.No Then
Timer1.Enabled = True
Exit Sub
End If
Timer1.Enabled = False
' continue delete the article from database
SQLite.RecordDelete("articles", "id", articleID, "integer")
' delete the folder
articlePath = articlesPath & "\" & articleID
Files.Dir_Delete(articlePath)
' delete the node from tree
SelectedArticle.Remove()
Timer1.Enabled = True
ReadArticle(articleID)
End If
End Sub
要删除文章,用户需要先选择它。会弹出一个消息框进行确认。确认后,文章将从 SQLite 数据库中删除,文章路径将被清除,并且树视图中链接到该文章的节点也会被移除。
1.3 文件 > 重命名
如果您想更改文章名称,请点击 File > Rename,然后在创建新文章时那样覆盖您的文章名称。只有文章的标题会被更改。
Private Sub cmdRenameArticle_Click(sender As Object, e As EventArgs) Handles cmdRenameArticle.Click
' user clearing the contents of the article
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
' there is no article selected
Common.MyMsgBox("You have not selected any article to rename yet!", "o", "e")
Else
' set the editing mode to true
Timer1.Enabled = False
treeArticles.LabelEdit = True
' begin the edit of the node
If Not SelectedArticle.IsEditing Then
SelectedArticle.BeginEdit()
End If
Timer1.Enabled = True
End If
End Sub
用户需要选择一篇文章进行重命名。完成后,将触发上面讨论的 LabelEdit 和 BeginEdit。这次的文章前缀是“article-”,因此文章将在数据库中更新,如 AfterLabelEdit 中所示。
1.4. 文件 > 重置
重置文章会清除文章的所有内容,并创建一个空白模板供您书写。只有当您想从头开始重写文章时才执行此操作。此操作无法撤销。
Private Sub cmdResetArticle_Click(sender As Object, e As EventArgs) Handles cmdResetArticle.Click
' user clearing the contents of the article
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
' there is no article selected
Common.MyMsgBox("You have not selected any article to reset yet!", "o", "e")
Else
Dim ans As MsgBoxResult = Common.MyMsgBox("Reset: " & SelectedArticle.Text & vbCrLf & vbCrLf & _
"Are you sure that you want to reset this article. All the article contents will be reset. You cannot undo this action. Continue?", "yn", "q")
Select Case ans
Case MsgBoxResult.Yes
Timer1.Enabled = False
' get the article file
articleFile = GetArticleFile()
' get the article id
articleID = GetArticleID()
' delete the article file
Files.File_Delete(articleFile)
' get the blank article contents
articleContent = Files.File_Data(blankArticle)
' write blank article to article
Files.File_Update(articleFile, articleContent)
' copy the main.css file to article folder
articlePath = articlesPath & "\" & articleID
articleBaK = articlePath & "\BAK"
Files.Dir_Create(articlePath)
Files.Dir_Create(articleBaK)
Files.File_CopyToFolder(Common.AppPath & "\quote.gif", articlePath)
' read the new article and display it
Timer1.Enabled = True
ReadArticle(articleID)
End Select
End If
End Sub
用户将被询问是否要重置文章。这将删除文章文件,并将文件内容重置为 CodeProject 的空白提交模板。
1.5. 文件 > 另存为
此功能与从 Internet 浏览器保存页面是相同的功能。选择后,您的文章将保存为单个 htm 文件。这会运行一个 ExecCommand,并将文章标题的文件名传递给它。
Private Sub SaveAsToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles SaveAsToolStripMenuItem.Click
Timer1.Enabled = False
txtContent.Document.ExecCommand("SaveAs", False, SelectedArticle.Text)
Timer1.Enabled = True
End Sub
1.6 文件 > 关闭
这将退出应用程序。不需要用户进行任何确认。
Private Sub cmdCloseApp_Click(sender As Object, e As EventArgs) Handles cmdCloseApp.Click
Application.Exit()
End Sub
2. 撰写和格式化您的文章
现在您已经创建了新文章,是时候撰写一些内容了。每次创建新文章时,都会使用 CodeProject 文章模板作为空白文章,并需要对其进行更新,以便您的文章有实质内容。
点击文章中的任何位置,您的鼠标指针将开始闪烁,以便您可以开始输入。在写作之前,您可能需要隐藏树视图和文章的 HTML 视图。为此,请点击 **TreeView Toggle** 和 **HTML Toggle** 按钮,如下图所示。
Private Sub ToolStripButton1_Click_1(sender As Object, e As EventArgs) Handles cmdHideTree.Click 'toggle the splitcontainer treeview visibility Splits() End Sub
Private Sub Splits() Dim bStatus As Boolean = SplitContainer1.Panel1Collapsed If bStatus = True Then SplitContainer1.Panel1Collapsed = False Else SplitContainer1.Panel1Collapsed = True End If ' ensure splitters are same width at 50% SplitContainer1.SplitterDistance = SplitContainer1.Width / 4 SplitContainer2.SplitterDistance = SplitContainer2.Width / 2 End Sub
HTML 切换按钮就在树视图切换按钮旁边。这将打开整个屏幕供您写作。现在,让我们继续撰写文章并进行格式化,以便发布。
正如您从上面看到的,树视图和 HTML 控件放置在拆分容器中。您可以通过运行每个拆分容器内的 PanelXCollapsed 方法来隐藏每个面板。我也会调整容器的大小,以便当它们一起显示时,Web 控件和 HTML 控件各占一半屏幕。
2.1 格式化标题
标题中的大多数控件都使用 webControl 可用的 ExecCommands。这些命令已在这里详细解释。
标题按钮具有格式化 H1-H6 标题的功能,以及移除格式、插入 ` 2.2 格式化源代码 您的文章中可能有一些源代码需要格式化。选择您的源代码,然后点击相应的格式以应用于最终用于发布的文章。您格式化的代码将像往常一样以橙色背景高亮显示,当您使用在线 CodeProject 编写器预览文章时,您将看到效果,如下所示。 在经历了每种编程语言的源代码格式化之后,我也想在此添加该功能。每种编程语言都有其自己的指示符。它们是: FormatBlock 无法正常工作以确保生成的 HTML 输出满足此要求,所以我写了一个小程序。 每个按钮都传递了相关的编程语言。首先,您选择要格式化的文本。从选定的文本创建一个 IHTMLtxtRange,然后将 formatblock 应用于该范围,并使用 ` 2.3 更改文本的前景色 选择要应用前色的文本,然后点击提供的按钮并选择您想要应用的一种颜色。要应用前色,我们调用 FontDialog。 和 颜色对话框允许用户选择他们想要的颜色,SetSelectionForeColor 将颜色应用于选定的文章文本。颜色会通过 ColorTranslator 转换为 HTML 格式。 2.4 更改字体属性 同样,就像颜色对话框一样,字体对话框用于更改文章中选定文本的字体。 和 字体名称将应用于文本,然后根据从字体对话框读取的属性应用粗体、斜体、下划线和字号属性。 2.5 插入链接 选择要应用链接的文本,然后点击 Link 按钮。复制或粘贴链接。要删除链接,您需要选择它,然后点击 unlink 按钮。要插入链接,我们调用 CreateLink ExecCommand 并告诉它显示该功能的用户界面。命令中的变量为 True。 2.6 您也可以在内容中插入水平分隔线。 2.7 插入图像 为了插入图像,我们也调用 ExecCommand 并要求它显示其用户界面。 2.8 插入表单控件 要插入表单控件,您需要使用上述 Microsoft 文章中关于 ExecCommands 的 JavaScript 部分。您可以在此处找到有关这些控件的更多详细信息。例如,为了插入一个文本框,我执行了以下操作: 2.9 插入锚点/书签 插入锚点/书签需要用户指示书签名。我使用了一个输入框来要求用户输入锚点名称。 3. 打印和预览您的文章 这些是 webbrowser 中的内置命令,但打印命令是一个 ExecCommand。 4. 文章朗读 为了朗读文章内容,我使用了 speech api。在初始化 speech 类后,我将可用的语音加载到工具栏中的组合框中。 然后要朗读文本,调用了... 从 这会检测选定的语音,然后朗读文章文本,如从 **txtContent.Document.Body.Parent.InnerText** 中读取的。 5. TreeView 文章详情 当 CodeProject.Show 启动时,主窗体加载,并且这段代码被执行。 正如您所见,它连接到一个名为 articles.db 的 SQLite 数据库。随后是 RefreshArticles,它将所有可用的文章加载到树视图中供选择,并将可用的语音加载到语音组合框中。 那么,看一下 SQLite 类做了什么很重要。我们开始吧。 这段代码将新记录插入到一个表中。我们使用一个字典对象来定义来自 Map 类的键值对。对于 map 中的每个字段,我们定义它如下: 并将 m 传递给此方法。 要更新,我们需要两个 map,一个用于字段值,一个用于 where 子句。 删除记录的工作方式也相同,我们传递一个 map 字段键值对给它,以供删除,然后调用 SQLite 中的 DeleteMap。 Map 对象 这是一个字典对象,定义如下: 并且 Put 函数只是更新字典。 上面的方法创建了 SQL 命令的一部分,用于使用参数更新表内的记录。 6. HTML 详情 HTML 详情是通过 ICSharpCode.TextEditor 显示的。颜色编码只需要一行代码即可完成,在控件的内容加载后。ReadArticle 方法调用一个名为 SetHTML 的方法,并将文章的内容传递给它。 要让控件将文本标记为 HTML,我们只需调用 **SetHighlighting**。 这是 Nuget 包,以及一个关于如何使用它的 CodeProject 文章。 7. 状态栏 状态栏显示了您文章的一些有趣统计信息。其中一项是 *Last Saved*(最后保存)项。每次自动保存文章时,此项都会更新。要使计时器工作,文章必须在树视图中被选中。 SaveContent 基本上做了这件事。它将文章的内容保存到 Articles 文件夹。 这会获取树中选定的 treenode,它应该是文章/图像。我们需要它的原因是因为图像和链接是可点击的,并且会在编写区域内打开,覆盖您的内容。因此,我们必须小心,因为计时器每 60,000 毫秒(即 1 秒)触发一次。 文章内容从 webbrowser 控件的 OuterHTML 属性读取。文件名被清理,文章文件被更新,并且在保存文档时,也会使用文档的时间和日期备份到 BAK 文件夹。这也会更新 HTML 查看器中的新内容。计时器在您输入文档时就会触发。 8. 文章位置 & 在浏览器中打开 要使用默认文件打开程序打开任何文档,需要调用 **Process.Start** 方法。 这里我们只需将要打开的文件名传递给进程,它就会使用默认应用程序打开它。 但是,要打开内置的 Windows 文件资源管理器,我们做了些别的事情。 我们调用了要启动的程序名称以及要打开的文件夹。 9. 将您的文章复制到 CodeProject 本节将讨论 CodeProject.Show 的发布部分。 完成文章撰写后,您需要将其发布到 CodeProject 在线。发布文章会将所有内容提取到记事本,以便您可以将 HTML 复制并粘贴到 CodeProject 网站的文章源代码中。UnsetArticleMedia 会清理所有图像链接,并删除图像链接中的完整路径。然后创建一个新的 publish.txt 文件来保存您的内容。**txtContent.Document.Body.InnerHTML** 包含您文档的 HTML 内容,您可以将其粘贴到 CodeProject 的“Source”(源代码)中。 这是我使用 CodeProject.Show 撰写的第二篇文章。由于一个 bug,我已将 treeArticles_AfterSelect 的代码更改为 NodeMouseClick。 各位读者,就到这里了。欢迎来到离线 CodeProject 文章写作的世界!!`、`
Private Sub H5ToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles H5ToolStripMenuItem.Click
Timer1.Enabled = False
txtContent.Document.ExecCommand("FormatBlock", False, "<h5>")
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
No Language = text
asp.net = aspnet
c# = cs
c++ = C++
c++/cli = mc++
css = css
f# = F#
html = html
java = Java
JavaScript = jscript
masm/asm = asm
msil = msil
midl = midl
php = php
sql = sql
vb.net = vb.net
vbscript = vbscript
xml = xml
Private Sub SetProgrammingLanguage(lang As String)
Timer1.Enabled = False
' element to set the attribute
Dim hElement As IHTMLElement
' get the document
Dim doc As IHTMLDocument2 = txtContent.Document.DomDocument
' get selected range
Dim range As IHTMLTxtRange = doc.selection.createRange()
' format the code
range.execCommand("FormatBlock", False, "<pre>")
' get the element
hElement = range.parentElement
' add attribute to element
hElement.setAttribute("lang", lang)
hElement.removeAttribute("class")
SaveContent()
Timer1.Enabled = True
End Sub
` 元素。然后读取包含文本的父元素,并传递一个带有相关语言的 language 属性。这会为您的选定内容产生类似如下的效果:
<PRE lang=html>contentEditable='true'</PRE>
Private Sub cmdChangeColor_Click(sender As Object, e As EventArgs) Handles cmdChangeColor.Click
Timer1.Enabled = False
' open the color selector
ColorDialog1.SolidColorOnly = True
ColorDialog1.AllowFullOpen = False
ColorDialog1.AnyColor = False
ColorDialog1.FullOpen = False
ColorDialog1.CustomColors = Nothing
Dim result As DialogResult = ColorDialog1.ShowDialog()
If result = Windows.Forms.DialogResult.OK Then SetSelectionForeColor(Me.ColorDialog1.Color)
Timer1.Enabled = True
End Sub
Private Sub SetSelectionForeColor(ByVal Color As System.Drawing.Color)
Timer1.Enabled = False
' choose a color for the text
txtContent.Document.ExecCommand("ForeColor", False, System.Drawing.ColorTranslator.ToHtml(Color))
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub cmdFont_Click(sender As Object, e As EventArgs) Handles cmdFont.Click
Timer1.Enabled = False
' open the font selector
Dim result As DialogResult = FontDialog1.ShowDialog()
' assign font to selection
If result = Windows.Forms.DialogResult.OK Then SetSelectionFont(Me.FontDialog1.Font)
Timer1.Enabled = True
End Sub
Private Sub SetSelectionFont(ByVal Font As System.Drawing.Font)
' set the font for the text
Timer1.Enabled = False
txtContent.Document.ExecCommand("FontName", False, Font.Name)
If Font.Bold And Not txtContent.Document.DomDocument.queryCommandValue("Bold") Then
txtContent.Document.ExecCommand("Bold", False, Nothing)
End If
If Font.Italic And Not txtContent.Document.DomDocument.queryCommandValue("Italic") Then
txtContent.Document.ExecCommand("Italic", False, Nothing)
End If
If Font.Underline And Not txtContent.Document.DomDocument.queryCommandValue("Underline") Then
txtContent.Document.ExecCommand("Underline", False, Nothing)
End If
txtContent.Document.ExecCommand("FontSize", False, ConvertFontSizeToHTMLFontSize(Font.SizeInPoints))
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub cmdCreateLink_Click(sender As Object, e As EventArgs) Handles cmdCreateLink.Click
Timer1.Enabled = False
txtContent.Document.ExecCommand("CreateLink", True, "")
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub cmdInsertHR_Click(sender As Object, e As EventArgs) Handles cmdInsertHR.Click
RunCommand("insertHorizontalRule")
End Sub
Private Sub InsertImageToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles InsertImageToolStripMenuItem.Click
Timer1.Enabled = False
txtContent.Document.ExecCommand("InsertImage", True, "")
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub TextToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles TextToolStripMenuItem.Click
Timer1.Enabled = False
txtContent.Document.ExecCommand("InsertInputText", False, "txt")
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub cmdAddAnchor_Click(sender As Object, e As EventArgs) Handles cmdAddAnchor.Click
Dim bmName As String = InputBox("Please enter the bookmark name below:", "BookMark Name", "BookMark1")
If Len(bmName) = 0 Then Exit Sub
Timer1.Enabled = False
txtContent.Document.ExecCommand("CreateBookmark", False, bmName)
txtContent.Document.Body.Focus()
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub ToolStripButton1_Click(sender As Object, e As EventArgs) Handles cmdPrintPreview.Click
txtContent.ShowPrintPreviewDialog()
End Sub
Private Sub cmdProperties_Click(sender As Object, e As EventArgs) Handles cmdProperties.Click
txtContent.ShowPropertiesDialog()
End Sub
Private Sub cmdPageSetup_Click(sender As Object, e As EventArgs) Handles cmdPageSetup.Click
txtContent.ShowPageSetupDialog()
End Sub
Public Shared Function GetVoices() As List(Of String)
Dim sVoices As New List(Of String)
Initialize()
For Each voice As System.Speech.Synthesis.InstalledVoice In synth.GetInstalledVoices()
sVoices.Add(voice.VoiceInfo.Name)
Next
Return sVoices
End Function
Public Shared Sub StartSpeaking(ByVal strVoice As String, ByVal strText As String)
synth.Rate = -2
synth.Volume = 100
synth.SelectVoice(strVoice)
synth.SpeakAsync(strText)
Paused = False
End Sub
Private Sub cmdTextAloud_Click(sender As Object, e As EventArgs) Handles cmdTextAloud.Click
' get the voice to use
Dim strVoice As String = cboVoices.SelectedItem
If Len(strVoice) = 0 Then
MyMsgBox("You need to select a voice engine to read the article first!", , , "Speech Engine Error")
Exit Sub
End If
' read contents of the article out aloud
Timer1.Enabled = False
Dim pContent As String = txtContent.Document.Body.Parent.InnerText
Speech.StartSpeaking(strVoice, pContent)
End Sub
Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles Me.Load
' run after the form is loaded
' ensure the splitters are resized
' ensure splitters are same width at 50%
SplitContainer1.SplitterDistance = SplitContainer1.Width / 4
SplitContainer2.SplitterDistance = SplitContainer2.Width / 2
' create a folder to hold articles
Files.Dir_Create(articlesPath)
' open the database
SQLite.UseDataFolder = False
SQLite.Database = "articles.db"
SQLite.OpenConnection()
' load the articles
RefreshArticles()
'set up scintilla
' set up voices
Dim mVoices As List(Of String) = Speech.GetVoices
' load voices
CboBoxFromCollection(cboVoices, mVoices, True)
End Sub
Shared Sub InsertMap(TableName As String, sm As Map)
Dim sb As New StringBuilder
sb.Append("INSERT INTO [" & TableName & "] (")
sb.Append(sm.Columns).Append(") VALUES (").Append(sm.Values).Append(")")
Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
sm.SetSqLiteCommand(sCommand)
sCommand.ExecuteNonQuery()
End Sub
dim m as new Map: m.put("article", "My First Article")
Shared Sub UpdateMap(TableName As String, sm As Map, wm As Map)
Dim sb As New StringBuilder
sb.Append("UPDATE [" & TableName & "] SET ")
sb.Append(sm.ColumnsUpdate).Append(" WHERE ").Append(wm.ColumnsUpdate)
Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
sm.SetSqLiteCommand(sCommand, True)
wm.SetSqLiteCommand(sCommand, False)
sCommand.ExecuteNonQuery()
End Sub
Shared Sub DeleteMap(TableName As String, wm As Map)
Dim sb As New StringBuilder
sb.Append("DELETE FROM [" & TableName & "] WHERE ")
sb.Append(wm.ColumnsUpdate)
Dim sCommand As SQLiteCommand = SQLite.OpenCommand(sb.ToString)
wm.SetSqLiteCommand(sCommand)
sCommand.ExecuteNonQuery()
End Sub
Public Class Map
Public MapDict As Dictionary(Of Object, Object)
Public IsInitialized As Boolean = False
Private Quote As String = Chr(34).ToString
Public Sub Put(sKey As Object, sValue As Object)
' update the key value pair in the dictionary
If MapDict.ContainsKey(sKey) = True Then
MapDict.Item(sKey) = sValue
Else
MapDict.Add(sKey, sValue)
End If
End Sub
Public Function ColumnsUpdate() As String
' define the update statement for the map
Dim cols As New List(Of String)
For Each pair As KeyValuePair(Of Object, Object) In MapDict
Dim sKey As String = pair.Key.ToString
cols.Add("[" & sKey & "] = @" & sKey)
Next
Return String.Join(",", cols)
End Function
Sub SetHTML(articleData As String)
' load the text to the text editor
' set highlight scheme to html
txtHTML.Text = articleData
txtHTML.SetHighlighting("HTML")
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
' save the contents of the document every 1 minute, ensure we are at article level
Timer1.Enabled = False
SaveContent()
Timer1.Enabled = True
End Sub
Private Sub SaveContent()
' saved content should be an article
If Len(txtContent.Tag) = 0 Then Exit Sub
Dim cArticle As TreeNode = treeArticles.SelectedNode
Dim bakFile As String
Dim bakDate As String
If TypeName(cArticle) <> "Nothing" Then
' get article key and id
Dim ak As String = cArticle.Name
Dim id As String = Common.MvField(ak, 2, "-")
ak = Common.MvField(ak, 1, "-")
Select Case ak
Case "article", "images"
'DocumentText does not reflect new changes made after execCommand
Dim pContent As String = txtContent.Document.Body.Parent.OuterHtml
pContent = pContent.Replace("{{article}}", articleTitle)
' save a backup file just in case ish happens
bakDate = DateTime.Now.ToLongDateString.Replace("/", "-").Replace("\", "-").Replace(":", "-")
bakDate = bakDate & " " & DateTime.Now.ToLongTimeString.Replace("/", "-").Replace("\", "-").Replace(":", "-")
bakFile = articlesPath & "\" & id & "\BAK\" & id & "-" & bakDate & ".html"
Call Files.File_Update(bakFile, pContent)
' save to the original file
Dim bSaved As Boolean = Files.File_Update(txtContent.Tag, pContent)
SetHTML(pContent)
Dim fsize As Long = Files.Dir_Size(articlePath)
Dim fsize1 As String = Files.File_SizeName(fsize)
StatusMessage(StatusBar, "Size: " & fsize1 & " |", 4)
' get last modification date
StatusMessage(StatusBar, "Last Saved: " & Files.File_LastModifiedTime(txtContent.Tag) & "|", 3)
End Select
End If
End Sub
Public Shared Sub File_View(ByVal sFileName As String, Optional ByVal Operation As String = "Open", Optional ByVal WindowState As Microsoft.VisualBasic.AppWinStyle = AppWinStyle.NormalFocus)
If File_Exists(sFileName) = False Then Exit Sub
Dim procStart As Process
Select Case Operation.ToLower
Case "open"
procStart = Process.Start(sFileName, WindowState)
procStart.WaitForExit()
Case "print"
Case Else
End Select
End Sub
Public Shared Sub OpenFolder(sFolder As String)
If Len(sFolder) = 0 Then Exit Sub
Process.Start("explorer.exe", sFolder)
End Sub
Private Sub PublishToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles PublishToolStripMenuItem.Click
' user clearing the contents of the article
SelectedArticle = treeArticles.SelectedNode
If TypeName(SelectedArticle) = "Nothing" Then
' there is no article selected
Common.MyMsgBox("You have not selected any article to publish yet!", "o", "e")
Else
Timer1.Enabled = False
HourGlassShow(Me)
articleID = GetArticleID()
UnsetArticleMedia(articleID)
SaveContent()
' get the content and put in clipboard
articlePublish = AppPath() & "\publish.txt"
Files.File_Update(articlePublish, txtContent.Document.Body.InnerHtml)
HourGlassHide(Me)
Timer1.Enabled = True
' open the new file
Files.File_View(articlePublish)
End If
End Sub
关注点