要解决的问题: 有多个线程输出日志,日志内容需要在列表框中显示出来,不管日志输出的频率快慢,界面不能卡,不能闪烁。超过10万行日志时,自动删除最开始的1万行日志。
此问题涉及多线程编程,多线程输出时要更新界面的显示。
多线程的东西,当然不能忘了QWorker这样的神器,下面我们就来使用QWorker解决问题,哦,不对,是用YxdWorker来解决问题(为什么是YxdWorker?哈,这是宝宝的修改版本啦,封装成了自己喜欢的样子,其实和QWorker差不多,有兴趣可以到 SVN: https://github.com/yangyxd/YxdWorker下载。)
一、先在在搜索路径中添加对YxdWorker的引用,然后在窗体上放一个ListView, Name=ListView1,再放一个CheckBox, Name=CheckBox1。效果如下:
ListView1的OwnerData属性设为True,ViewStyle属性设为vsReport,添加一列“日志内容”。
二、 开始写代码
unit Unit1; interface uses YxdWorker, SyncObjs, CommCtrl, Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls; type TForm1 = class(TForm) CheckBox1: TCheckBox; ListView1: TListView; Button1: TButton; Button2: TButton; Button3: TButton; Button4: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure ListView1Data(Sender: TObject; Item: TListItem); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure Button4Click(Sender: TObject); procedure Button1Click(Sender: TObject); procedure CheckBox1Click(Sender: TObject); private { Private declarations } FLogs: TStrings; // 存放日志内容 FAutoScroll: Boolean; // 是否自动滚动 FLogsIsDel: Boolean; // 是否已经删除日志 FLogRef: Integer; // 状态计数器 FLocker: TCriticalSection; FTestRef: Integer; public { Public declarations } procedure DoDataChange(); procedure DoWriteLog(Sender: TObject; const Log: string); procedure OnDataChange(AJob: PJob); procedure DoTest(AJob: PJob); procedure Log(const Text: string); end; var Form1: TForm1; implementation {$R *.dfm} { TForm1 } procedure TForm1.Button1Click(Sender: TObject); begin Workers.Post(DoTest, nil); end; procedure TForm1.Button2Click(Sender: TObject); begin FLocker.Enter; FLogs.Clear; FLogsIsDel := True; FLocker.Leave; DoDataChange(); end; procedure TForm1.Button3Click(Sender: TObject); begin Log('写一行日志'); end; procedure TForm1.Button4Click(Sender: TObject); begin Workers.Clear(DoTest, nil); end; procedure TForm1.CheckBox1Click(Sender: TObject); begin FLocker.Enter; FAutoScroll := CheckBox1.Checked; FLocker.Leave; end; procedure TForm1.DoDataChange; begin // FLogRef 很关键,决定什么时候真正更新显示。 // 多线程写日志时,FLogRef > 1,那么也只更新一次。 if InterlockedIncrement(FLogRef) = 1 then // 延时50ms更新。此值设定的越大,列表更新的越慢。 // 不延时的话,界面刷新太快,占用资源会比较大 Workers.Post(OnDataChange, nil, True, 50) else InterlockedDecrement(FLogRef); end; procedure TForm1.DoTest(AJob: PJob); var I, M: Integer; begin M := 0; while (not AJob.IsTerminated) and (M < 10000) do begin Inc(M); I := InterlockedIncrement(FTestRef); Log(Format('日志内容。工作者:%d. (%d)', [AJob.Handle, I])); Sleep(100); end; end; procedure TForm1.DoWriteLog(Sender: TObject; const Log: string); var I: Integer; begin if Assigned(FLogs) and (Assigned(Self)) then begin FLocker.Enter; // 大于10万行时,删除前面的1万行 if FLogs.Count > 100000 then begin FLogsIsDel := True; for I := 10000 downto 0 do FLogs.Delete(I); end; // 添加当前日志内容 FLogs.Add('[' + FormatDateTime('hh:mm:ss.zzz', Now) + '] ' + Log); FLocker.Leave; // 产生一个变更通知 DoDataChange(); end; end; // 初始化 procedure TForm1.FormCreate(Sender: TObject); begin FLogsIsDel := False; FLogRef := 0; FTestRef := 0; FLogs := TStringList.Create(); FLocker := TCriticalSection.Create; FAutoScroll := CheckBox1.Checked; // 将最大工作者数量设置大一些。因为太少的话,工作者不够用,没有时间来显示日志了。 Workers.MaxWorkers := 512; end; procedure TForm1.FormDestroy(Sender: TObject); begin Workers.Clear(Self); FreeAndNil(FLogs); FreeAndNil(FLocker); end; procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem); begin FLocker.Enter; if Assigned(FLogs) and (Item.Index < FLogs.Count) then Item.Caption := FLogs[Item.Index]; FLocker.Leave; end; procedure TForm1.Log(const Text: string); begin if Assigned(Self) then DoWriteLog(Self, Text); end; procedure TForm1.OnDataChange(AJob: PJob); begin if Assigned(Self) and Assigned(FLogs) then begin if Assigned(ListView1) and (ListView1.HandleAllocated = True) then begin ListView_SetItemCountEx(ListView1.Handle, FLogs.Count, LVSICF_NOINVALIDATEALL or LVSICF_NOSCROLL); // 修改列表项的数量,并不改变滚动条位置 if FAutoScroll then SendMessage(ListView1.Handle, WM_VSCROLL, SB_BOTTOM, 0); if FLogsIsDel then begin FLogsIsDel := False; ListView1.Invalidate; end; end; InterlockedDecrement(FLogRef); end; end; end.
核心有两点:
- 使用FLogRef计算器作为实际更新界面的依据。
- 使用ListView_SetItemCountEx宏来代替ListView.Count=xxxx更新列表框的行数。
- 使用临界锁来保持线程同步。
源码下载: