要解决的问题: 有多个线程输出日志,日志内容需要在列表框中显示出来,不管日志输出的频率快慢,界面不能卡,不能闪烁。超过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更新列表框的行数。
- 使用临界锁来保持线程同步。
源码下载:
