讲述如何开发一个控件,很有价值(三)

本文围绕Delphi展开,介绍了RTF流的格式及代码重写,解决了ScanForRtf的崩溃问题。还探讨了自动语法高亮的实现,包括选择触发事件、分析文本信息获取方式,借助WinAPI调用解决RichEdit控件访问问题,并得出两条Delphi规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

blank.rtf - empty -so I could see the "plain" header line

{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS Sans Serif;}{/f1/froman/fcharset2 Symbol;}{/f2/froman Times New Roman;}} 
{/colortbl/red0/green0/blue0;}/deflang1033/pard/plain/f2/fs20 /par }

plaintext.rtf - too see how having any text was handled

{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS Sans Serif;}{/f1/froman/fcharset2 Symbol;}{/f2/froman Times New Roman;}}{/colortbl/red0/green0/blue0;} 
/deflang1033/pard/plain/f2/fs20 this is plain text
/par }

difffont.rtf - different font, same size, same text

{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS Sans Serif;}{/f1/froman/fcharset2 Symbol;}{/f2/froman Times New Roman;}{/f3/fswiss/fprq2 Arial;}}{/colortbl/red0/green0/blue0;} 
/deflang1033/pard/plain/f3/fs20 plain text different font/plain/f2/fs20
/par }

diffsize.rtf - text set to 18 point in the default font

{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS Sans Serif;}{/f1/froman/fcharset2 Symbol;}{/f2/froman Times New Roman;}}{/colortbl/red0/green0/blue0;} 
/deflang1033/pard/plain/f2/fs36 plain text different font/plain/f2/fs20
/par }

diffcolor.rtf - etc. my favourite of course - blue.

{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS Sans Serif;}{/f1/froman/fcharset2 Symbol;}{/f2/froman Times New Roman;}}{/colortbl/red0/green0/blue0;/red0/green0/blue255;
/deflang1033/pard/plain/f2/fs20/cf1 plain text different font/plain/f2/fs20 
/par }

Looking at the resultant codes you see how the RTF stream is formatted. It comprises a:

  1. INITIAL HEADER          (/rtf1/.....)
  2. FONTTABLE                  (/f0/fswiss...)
  3. COLORTABLE              (/colortbl)
  4. MISCELLANEOUS
  5. DEFAULT FORMAT      (/pard....)
  6. BODY OF THE FILE.

As a result of that I rewrote this code:

WriteToBuffer('{/rtf1/ansi/deff0/deftab720{/fonttbl{/f0/fswiss MS
SansSerif;}{/f1/froman/fcharset2 Symbol;}{/f2/fmodern Courier New;}}'+#13+#10);
WriteToBuffer('{/colortbl/red0/green0/blue0;}'+#13+#10);
WriteToBuffer('/deflang1033/pard/plain/f2/fs20 ');

to become:

WriteToBuffer('{/rtf1/ansi/deff0/deftab720');
WriteFontTable;
WriteColorTable;
WriteToBuffer('/deflang1033/pard/plain/f0/fs20 ');

The procedures Write[Font,Color]Table basically creates a table of fonts/colors we can reference later on. Each Font and Color type is stored by index in a TList internally. It acts as a lookup tables - by matching the Font name or Color value we can find the [num] to code into the RTF stream at the required moment:

/f[num]      =      the index of which Font you want to use, as pre-set in the "on the fly" font table
/fs[num]    =     point size - (for example 20 = 10point)
/cf[num]    =     the index of which Color to use, as preset in "on the fly" color table
/cb[num]   =     which background color to use - (ignored in RichEdit version 2.0)

PROBLEM#2 Crashes in long comments or text (existing problem)
 

There is a bug in ScanForRtf. Can you see it?
 

procedure TPasConversion.AllocStrBuff;
begin

FStrBuffSize:= FStrBuffSize + 1024;
ReAllocMem(FStrBuff, FStrBuffSize);
FStrBuffEnd:= FStrBuff + 1023;

end; { AllocStrBuff }

procedure TPasConversion.ScanForRtf;
var
       i: Integer;
begin

     RunStr:= FStrBuff;
     FStrBuffEnd:= FStrBuff + 1023;

     for i:=1 to TokenLen do
     begin
          Case TokenStr[i] of
               '/', '{', '}':
               begin
                    RunStr^:= '/';
                    inc(RunStr);
               end
          end;

          if RunStr >= FStrBuffEnd then AllocStrBuff;
          RunStr^:= TokenStr[i];
          inc(RunStr);
     end;

     RunStr^:= #0;
     TokenStr:= FStrBuff;

end; { ScanForRtf }

EXAMPLE - code snippet from Pas2Rtf demonstrating the "long comment" bug

The problem: if FStrBuff is enlarged using AllocStrBuff() (to make it bigger to handle a very long comment) the Windows Memory manager probably has to re-allocate it by moving the entire string buffer somewhere else in memory. RunStr however is not adjusted for this change and stillpoints to the old memory area, now unallocated.

The fix: Reallocate RunStr in the AllocStrBuff routine so it points to the correct place in the new area of memory. Try and fix it yourself, or look at my garsely spaghetti code in jhdPasToRtf.pas.
 
 

Automatic Syntax Highlighting (my first implementation)
 

To understand how Automatic syntax highlighting works, you should have a close look at what happens in the Delphi 3.0 Editor. After all - if Borland was happy with it - who am I to argue :-)

Take note when the "syntax" changes and what is affected. In retrospect the difficult thing is to implement a highlighter that is:

  1. Fast
  2. Accurate
  3. Doesn't flicker
  4. Isn't obvious ("the someone is chasing me phenomenon".. you'll see)

1. When should we do the re-highlighting ?

In YourPasEdit the highlighting is done as the file is read in. Once this is done, the only way to make use of that technique would be to write out the file everytime it changes and read it back in again - obviously a very slow process. In my case, I basically wanted to just reformat the line(s) that have been changed, immediately after the change had been done i.e. after every new character, DELETE or BACKSPACE or even Paste or DragDrop had been processed. I needed something that was triggered everytime the control was effected in such a way.

What I needed then was an [Event].

2. Which event - there's so many to choose from ?

A RichEdit, like any control, has a number of [Events] triggered when you do various things to the control. What is not obvious, is that many events trigger other events in turn. So in choosing which Event(s) to hang your code off you have to ensure that (a) it catches all situations where you need to "fix" the highlighting and (b) it doesn't become re-entrant (i.e. what you do in the [Event], doesn't trigger itself again or any other [Event] that would call the "highlighting code"). From a quick look at the helpfile, I decided that [OnChange] seemed a likely candidate. According to the Delphi 3.0 Helpfile:
 

Write an OnChange event handler to take specific action whenever the text for the edit control may have changed. Use the Modified property to see if a change actually occurred. The Text property of the edit control will already be updated to reflect any changes. This event provides the first opportunity to respond to modifications that the user types into the edit control.

You may be thinking however: "Heh? What about those other things - like Methods and Properties. Can't they also change the text?" They sure can - but most end up triggering [OnChange] anyhow.

3. Is it what I want? - Rich text controls (from Delphi3 Helpfile)
 

The rich text component is a memo control that supports rich text formatting. That is, you can change the formatting of individual characters, words, or paragraphs. It includes a Paragraphs property that contains information on paragraph formatting. Rich text controls also have printing and text-searching capabilities.

By default, the rich text editor supports

  • Font properties, such as typeface, size, color, bold, and italic format
  • Format properties, such as alignment, tabs, indents, and numbering
  • Automatic drag-and-drop of selected text
  • Display in both rich text and plain text formats. 

  • (Set PlainText to True to remove formatting)

type TNotifyEvent = procedure(Sender: TObject) of object;
property OnChange: TNotifyEvent;

4. Is it the event I want - ie [OnChange] Event - the right one?

Live dangerously, let’s give it a go and see...by testing our assumptions out:

So I wrote my first [OnChange] event:

  1. Create a New application
  2. place on it one RichEdit (RichEdit1) and one Edit control (Edit1)
  3. Code the [OnChange] for the RichEdit1 control like this:
procedure TForm1.RichEdit1Change(Sender: TObject);
begin
  TRichEdit(Sender).Tag := TRichEdit(Sender).Tag + 1;
  Edit1.Text := 'Tag=' + IntToStr(TRichEdit(Sender).Tag);
end;

In this case the Sender object is the RichEdit being changed. The code basically uses the RichEdit's Tag variable (initially 0) as a handy Control specific variable. Everytime the [OnChange] event is called, it increases the Tag by 1, and display its value in an Edit Control as Text. You should pre-set the RichEdit control with some text in it, otherwise the following may be confusing!

  1. Compile and Run...
  2. Click in the Control. Nothing...
  3. Move around in it using CursorKeys... Nothing...
  4. Click outside the control.. and then back inside.. Nothing...
  5. Press the [Space Bar].. Tag=1...
  6. Press [Backspace].. Tag=2...
  7. Press return.. Tag=3..
  8. Select some text.. No change..
  9. CTRL-C some text.. No change..
  10. CTRL-X some text.. Tag=4..
  11. CTRL-V some text.. Tag=5..

As it looked good so far I then added to the Form1.OnShow event:

RichEdit1.Lines.LoadFromFile('c:/winzip.log');       {Just a plain text file hanging around }

to see what happened. And guess what - an [OnChange] event was called sometime and "Tag=1" was displayed in the Edit control as the proof when the Form appears for the first time. So we can see that procedures do call Events that apply to what they are doing.

5. What happens in Syntax Highlighting anyhow?

Watch carefully in the Delphi Editor. Now try and reproduce it. Open a WordPad (since WordPad is a souped up RichEdit basically). Read in a source file (e.g any Unit1.pas) and do syntax highlighting manually:

  1. Select a token
  2. Manipulate it using the buttons provided to change Font, Size, Color, and Bold
  3. Move onto the next token
  4. Goto 1

So therefore in [OnChange] we'll try and write code to reproduce what we have done manually.

6. Which text do I want.. and where do I get it ?

Hunting through the Delphi Helpfile on RichEdit controls we find that the actual text information in the RichEdit control is stored (or rather can be accessed from) either:
 

RichEdit.Text

Text contains a text string associated with the control.

TCaption = type string;
property Text: TCaption;

Description

Use the Text property to read the Text of the control or specify a new string for the Text value. By default, Text is the control name. For edit controls and memos, the Text appears within the control. For combo boxes, the Text is the content of the edit control portion of the combo box. 

RichEdit.Lines

Lines contains the individual lines of text in the rich text edit control.

property Lines: TStrings;

Description

Use Lines to manipulate the text in the rich text edit control on a line by line basis. Lines is a TStrings object, so TStrings methods may be used for Lines to perform manipulations such as counting the lines of text, adding lines, deleting lines, or replacing the text in lines.

To work with the text as one chunk, use the Text property. To manipulate individual lines of text, the Lines property works better.

Now Lines seemed to be what I wanted - after all I wanted the Syntax highlighting to work on a line by line basis. So let’s have a look at whats been changed.

Oh.. look at what? How can I tell which line is the one that is changed?

Unlike some Events, the [OnChange] isn't passed any variable's save the identity of the RichEdit control affected. The RichEdit Control doesn't have a runtime variable that tells us either. The only variables are the SelStart and SelLength - but their about selecting text aren't they? I just want to know what line I'm on :-(

It was about then that I re-read the information on the Sel??? properties, and recalled my "concept" code. Selection - I realised - was the name of the game. By manipulating these variables I could reproduce what I was doing manually - selecting text - as program code. Once selected you can then manipulate the attributes of the selected text through the SelAttributes structure.

Let’s get familiar with these variables (in Summary)
.

SelStartPosition of the Cursor, or the beginning of the selected text
SelLength0 if SelStart = Cursor Pos, or length of selected Text
SelTextempty if no text selected, or actual text selected
SelAttributeDefault attributes if I was to start typing at the Cursor position OR the actual attributes of the selected text

There is actually no other way to access the attributes of the text already in the Control than by programmatically accessing them via manipulating Sel variables (*if you stick to using the defined properties and methods).

In the end RichEdit.Text and RichEdit.Lines are just plain old strings - not really "rich" at all. The other thing to note is that SelStart is a 0-based index on the first character of RichEdit.Text - so it looks like Richedit.Line is out the door.

7. Okay implement: Select a Token

Basically I wanted to start at the beginning of the line, send just that line to PasCon, and read it back in and replace the current line with the result. Trouble is RichEdit doesn't give you access to the 'RTF' representation of a single line. Plus I still can't tell when the beginning or end of the line is. Since the latter seems to be a nagging problem, we better fix that first - trouble is: How?

When all else fails - WinAPI calls of course.

Most visual controls in Delphi are in fact just native Windows controls encapsulate as Delphi types. You can still use Windows API functions to access the control underneath. This was it is possible to access information not accessable per Delphi public Properties, Method or Events. Time to delve through Win32.HLP and see what it has to say about RichEdit controls. Its stored in C:/Program Files/Borland/Delphi 3.0/Help if you don't have a shortcut to it.

Open Win32.HLP -> [ Contents ] -> [ RichEdit controls ] -> [ Rich Edit Controls ]

I spent some time getting to know the "full" capabilites of the RichEdit control hidden behind Delphi's implementation of it. Much of what I learned came in handy later on (as you'll see) and as a result I derived my second two Delphi Rules:

Delphi Rule #2: If your Project hinges on the capabilities of a certain control - make sure you know everthing about it - from the beginning.

Delphi Rule #3: "Reference" is not the same as "Summary" (also known as Win32.HLP Rule#1)

Eventually I discovered the key in the [ Rich Edit Control Reference ] under "Lines and Scrolling". I had thought this page was simply a summary of the messages discussed in the preceding help pages. Actually it included a number of extra messages not discussed elsewhere - the exact ones I was after!
 

Lines and Scrolling

EM_LINEFROMCHAR - give them a 0-based index and they'll return the line 

EM_LINEINDEX - give them a line and you get the index of the first character 

EM_LINELENGTH - give them a line and you get the length of the line

So lets start coding.

(NB: To use the constants (EM-?) you'll have to manually add RichEdit in the uses clause)
 

procedure TForm1.RichEdit1Change(Sender: TObject);

  var WasSelStart,Row,BeginSelStart,EndSelStart: Integer;
  MyRe : TRichEdit;

begin

   MyRe          := TRichEdit(Sender);
   WasSelStart  := MyRE.SelStart;
   Row          := MyRE.Perform(EM_LINEFROMCHAR, MyRE.SelStart, 0);
   BeginSelStart:= MyRe.Perform(EM_LINEINDEX, Row, 0);
   EndSelStart  := BeginSelStart + Length(MyRE.Lines.Strings[Row]);

  // I didn't use the EM_LINELENGTH message, as the variables was avaiable via Delphi

   Edit1.Text := IntToStr(WasSelstart) + '-' +
   IntToStr(Row) + '-' +
   InttoStr(BeginSelStart) + '-' +
   IntToStr(EndSelStart);

end;

<think>嗯,用户想在Windows系统下设置Streamlit应用的自动启动,并且需要指定运行参数,比如--server.address 0.0.0.0--server.port 6006。我需要先理清楚Windows下自动启动程序的方法有哪些。常见的可能有任务计划程序、启动文件夹或者服务的方式。 首先,启动文件夹可能比较简单,用户可以将一个批处理文件放到启动文件夹里,这样登录时就会运行。但可能的问题是需要用户登录才会触发,如果希望即使不登录也能运行,可能需要用任务计划程序或者服务。 任务计划程序的话,可以设置触发器为系统启动时,这样不需要用户登录。不过可能需要配置正确的权限和操作。服务的方式可能更复杂,需要第三方工具如NSSM来创建服务,但稳定性更好。 然后要考虑如何正确执行streamlit命令。用户需要运行的是streamlit run your_script.py并带上参数。这里需要注意路径问题,可能需要用绝对路径来避免出错。比如,如果脚本不在当前目录,或者环境变量没有设置,可能导致命令找不到。所以批处理文件中应该使用全路径,或者先激活虚拟环境。 另外,用户可能希望应用在后台运行,不显示命令行窗口。这时候使用VBScript或者PowerShell脚本可以隐藏窗口,但可能需要更多的步骤。或者使用任务计划程序运行时设置不打开窗口。 还要注意防火墙设置,如果用户需要外部访问,可能需要开放对应的端口,比如6006,这在之前的引用中提到过类似的问题,引用[4]提到端口8080的设置,这里可以类比。 另外,用户提到的参数--server.address 0.0.0.0在引用[1]中有说明,正确使用这个参数可以让其他机器访问应用。同时指定端口6006,可能需要检查该端口是否被占用,或者是否需要配置路由器的端口转发。 总结步骤应该是:创建批处理文件,测试能否正常运行,然后将批处理文件或通过任务计划程序设置自动启动。同时可能需要隐藏窗口,使用VBScript或任务计划程序的设置。此外,提醒用户注意路径、权限和端口配置。</think>以下是在Windows系统下设置Streamlit应用自动启动并指定运行参数的完整教程: ### 一、创建启动脚本 1. 新建批处理文件 `start_streamlit.bat` ```bat @echo off cd /d "D:\your_project_path" streamlit run your_script.py --server.address 0.0.0.0 --server.port 6006 ``` (注意替换实际项目路径和脚本名称) 2. 可选隐藏窗口版本(使用VBScript): 创建 `start_hidden.vbs` ```vbscript Set WshShell = CreateObject("WScript.Shell") WshShell.Run "cmd /c D:\path\to\start_streamlit.bat", 0 ``` ### 二、设置自动启动方法 #### 方法1:任务计划程序(推荐) 1. 搜索并打开"任务计划程序" 2. 创建任务: - 常规:勾选"不管用户是否登录都要运行" - 触发器:新建"启动时" - 操作:启动程序选择`start_streamlit.bat` - 条件:取消所有勾选 - 设置:勾选"如果任务失败,重新启动",间隔5分钟 #### 方法2:启动文件夹 1. 按`Win+R`输入`shell:startup` 2. 将`start_hidden.vbs`复制到此目录 (仅当用户登录时生效) ### 三、验证配置 1. 手动运行批处理文件测试参数有效性 2. 检查防火墙设置,开放TCP 6006端口[^4] 3. 使用`netstat -ano|findstr 6006`验证端口监听状态 ### 四、补充说明 1. Python环境建议使用虚拟环境并固定路径[^3] 2. 参数`--server.address 0.0.0.0`确保外网可访问[^1] 3. 后台运行时建议禁用浏览器自动打开: ```bat streamlit run your_script.py --server.address 0.0.0.0 --server.port 6006 --global.disableWatchdogWarning true --browser.gatherUsageStats false ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值