Delphi RichEdit多CFE_LINK效果添加问题:仅最后链接生效
解决Delphi RichEdit插入多CFE_LINK链接仅最后一个生效的问题
嘿,我帮你分析下这个问题哈——你遇到的情况是每次插入新的CFE_LINK格式文本后,之前的链接都失效了,这大概率是因为插入新链接时没正确维护RichEdit的字符格式,再加上原代码里没跟踪每个链接的位置范围,导致点击识别也出问题。我给你修正了代码,能完美实现多链接正常生效的功能:
问题根源拆解
- CHARFORMAT2掩码设置不全:之前插入链接时可能没保留原有格式,直接覆盖了之前的链接标记,导致旧链接失效。
- 链接信息无位置关联:原代码的
TZ_RichEditLinks只存了文本和事件,但没记录每个链接在RichEdit里的起始位置和长度,点击时根本找不到对应的旧链接。 - 单例模式的局限:原代码用单例
FInstance,如果有多个RichEdit控件的话会直接混乱,改成实例列表就没问题了。
修正后的完整代码
unit uRichEditExtended; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, RichEdit, WinApi.ShellApi, Vcl.Controls, Vcl.ComCtrls, Generics.Collections; type TZ_RichEditClickEvent = reference to procedure(const ALinkText: string); TZ_RichEditLink = class IsDefaultEvent: boolean; Text: string; StartPos: Integer; // 新增:记录链接起始位置 Length: Integer; // 新增:记录链接长度 OnLinkClickEvent: TZ_RichEditClickEvent; end; TZ_RichEditLinks = TList<TZ_RichEditLink>; TRichEditExtended = class protected class var FInstances: TObjectList<TRichEditExtended>; // 改成实例列表支持多RichEdit FPrevRichEditWndProc: TWndMethod; FRichEdit: TRichEdit; FRichEditLinks: TZ_RichEditLinks; procedure InsertLinkText(const LinkText: string; SelStart: integer = -1); procedure SetRichEditMasks; procedure RichEditWndProc(var Message: TMessage); procedure AfterConstruction; override; procedure BeforeDestruction; override; constructor Create(ARichEdit: TRichEdit); public class function GetInstance(ARichEdit: TRichEdit): TRichEditExtended; class procedure ApplyRichEdit(ARichEdit: TRichEdit); class function AddLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer = -1): integer; class function AddLinkTextWithDefaultEvent(AText: string; SelStart: integer = -1): integer; class procedure RemoveRichEdit(ARichEdit: TRichEdit); end; implementation { TRichEditExtended } constructor TRichEditExtended.Create(ARichEdit: TRichEdit); begin inherited Create; FRichEdit := ARichEdit; FRichEditLinks := TZ_RichEditLinks.Create; SetRichEditMasks; // 替换RichEdit的窗口过程 FPrevRichEditWndProc := FRichEdit.WindowProc; FRichEdit.WindowProc := RichEditWndProc; end; procedure TRichEditExtended.AfterConstruction; begin inherited; if not Assigned(FInstances) then FInstances := TObjectList<TRichEditExtended>.Create(True); FInstances.Add(Self); end; procedure TRichEditExtended.BeforeDestruction; begin // 恢复原窗口过程 FRichEdit.WindowProc := FPrevRichEditWndProc; FRichEditLinks.Free; inherited; end; class procedure TRichEditExtended.ApplyRichEdit(ARichEdit: TRichEdit); begin GetInstance(ARichEdit); // 为目标RichEdit创建/获取实例 end; class function TRichEditExtended.AddLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer): integer; var Instance: TRichEditExtended; Link: TZ_RichEditLink; begin Instance := GetInstance(ARichEdit); Instance.InsertLinkText(AText, SelStart); // 保存链接的完整信息(包括位置) Link := TZ_RichEditLink.Create; Link.Text := AText; Link.IsDefaultEvent := False; Link.OnLinkClickEvent := AOnLinkClickEvent; if SelStart = -1 then Link.StartPos := Instance.FRichEdit.GetTextLen - Length(AText) else Link.StartPos := SelStart; Link.Length := Length(AText); Instance.FRichEditLinks.Add(Link); Result := Link.StartPos; end; class function TRichEditExtended.AddLinkTextWithDefaultEvent(AText: string; SelStart: integer): integer; var Instance: TRichEditExtended; Link: TZ_RichEditLink; begin Instance := GetInstance(ARichEdit); Instance.InsertLinkText(AText, SelStart); Link := TZ_RichEditLink.Create; Link.Text := AText; Link.IsDefaultEvent := True; if SelStart = -1 then Link.StartPos := Instance.FRichEdit.GetTextLen - Length(AText) else Link.StartPos := SelStart; Link.Length := Length(AText); Instance.FRichEditLinks.Add(Link); Result := Link.StartPos; end; class function TRichEditExtended.GetInstance(ARichEdit: TRichEdit): TRichEditExtended; var I: Integer; begin Result := nil; if not Assigned(FInstances) then FInstances := TObjectList<TRichEditExtended>.Create(True); // 查找目标RichEdit对应的实例 for I := 0 to FInstances.Count - 1 do if FInstances[I].FRichEdit = ARichEdit then begin Result := FInstances[I]; Break; end; // 没找到就创建新实例 if not Assigned(Result) then Result := TRichEditExtended.Create(ARichEdit); end; class procedure TRichEditExtended.RemoveRichEdit(ARichEdit: TRichEdit); var I: Integer; begin if Assigned(FInstances) then for I := FInstances.Count - 1 downto 0 do if FInstances[I].FRichEdit = ARichEdit then begin FInstances.Delete(I); Break; end; end; procedure TRichEditExtended.InsertLinkText(const LinkText: string; SelStart: integer); var CharFormat: TCharFormat2; SaveSel: TSelection; begin with FRichEdit do begin // 先保存当前选区,避免插入后破坏用户的选中状态 SaveSel := SelAttributes.Selection; try // 设置插入位置:-1表示插在末尾,否则插在指定位置 if SelStart = -1 then Self.SelStart := GetTextLen else Self.SelStart := SelStart; SelLength := 0; // 插入链接文本 SelText := LinkText; // 选中刚插入的文本,准备设置格式 Self.SelStart := Self.SelStart - Length(LinkText); SelLength := Length(LinkText); // 配置链接格式:保留颜色和链接掩码,避免覆盖原有格式 FillChar(CharFormat, SizeOf(CharFormat), 0); CharFormat.cbSize := SizeOf(CharFormat); CharFormat.dwMask := CFM_LINK or CFM_COLOR; CharFormat.dwEffects := CFE_LINK; CharFormat.crTextColor := clBlue; // 设置链接的蓝色显示 SendMessage(Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(@CharFormat)); finally // 恢复之前的选区 SelAttributes.Selection := SaveSel; end; end; end; procedure TRichEditExtended.SetRichEditMasks; var Mask: DWORD; begin // 开启RichEdit的链接事件监听,确保能捕获点击事件 Mask := SendMessage(FRichEdit.Handle, EM_GETEVENTMASK, 0, 0); SendMessage(FRichEdit.Handle, EM_SETEVENTMASK, 0, Mask or ENM_LINK); end; procedure TRichEditExtended.RichEditWndProc(var Message: TMessage); var ENLink: TENLink; I: Integer; ClickedPos: Integer; Link: TZ_RichEditLink; begin if Message.Msg = WM_NOTIFY then begin with PNMNotify(Message.LParam)^ do if code = EN_LINK then begin ENLink := PENLink(NMHdr)^; if ENLink.msg = WM_LBUTTONUP then begin // 获取点击位置对应的字符索引 ClickedPos := SendMessage(FRichEdit.Handle, EM_CHARFROMPOS, 0, LPARAM(@ENLink.pt)); // 遍历链接列表,找到点击的是哪个链接 for I := 0 to FRichEditLinks.Count - 1 do begin Link := FRichEditLinks[I]; if (ClickedPos >= Link.StartPos) and (ClickedPos < Link.StartPos + Link.Length) then begin // 触发对应事件:默认事件打开URL,自定义事件调用传入的回调 if Link.IsDefaultEvent then ShellExecute(0, 'open', PChar(Link.Text), nil, nil, SW_SHOWNORMAL) else if Assigned(Link.OnLinkClickEvent) then Link.OnLinkClickEvent(Link.Text); Break; end; end; end; end; end; // 一定要调用原窗口过程,保证RichEdit的其他功能正常 FPrevRichEditWndProc(Message); end; initialization TRichEditExtended.FInstances := nil; finalization TRichEditExtended.FInstances.Free; end.
关键修正点说明
- 新增位置跟踪:给
TZ_RichEditLink加了StartPos和Length,每个链接都记录自己在RichEdit里的位置范围,点击时能精准匹配。 - 格式设置优化:插入链接前先保存当前选区,插入后只给新文本设置链接格式,不会影响之前的内容。
CHARFORMAT2的掩码同时包含CFM_LINK和CFM_COLOR,确保链接样式正确且不覆盖原有格式。 - 多实例支持:把单例改成实例列表,多个RichEdit控件可以各自使用扩展功能,不会互相干扰。
- 点击事件精准匹配:通过
EM_CHARFROMPOS获取点击的字符位置,再遍历链接列表找到对应的链接,触发正确的事件。
使用示例
// 先给RichEdit绑定扩展功能 TRichEditExtended.ApplyRichEdit(RichEdit1); // 添加带自定义点击事件的链接 TRichEditExtended.AddLinkText('我的第一个链接', procedure(const ALinkText: string) begin ShowMessage('你点击了:' + ALinkText); end); // 添加默认打开URL的链接 TRichEditExtended.AddLinkTextWithDefaultEvent('https://www.example.com'); // 再插一个链接到指定位置(比如第10个字符后面) TRichEditExtended.AddLinkText('中间的链接', procedure(const ALinkText: string) begin ShowMessage('中间链接被点击啦'); end, 10);
这样修改后,你插入的每一个CFE_LINK链接都会正常保持效果,点击时也能正确触发对应的事件啦~
内容的提问来源于stack exchange,提问作者notricky
相关产品推荐
相关产品推荐

