1
0
mirror of https://github.com/loginov-dmitry/multithread.git synced 2024-11-24 16:53:48 +02:00
multithread/CommonUtils/ProgressViewer.pas

1231 lines
38 KiB
ObjectPascal

{ *************************************************************************** }
{ }
{ }
{ }
{ Модуль ProgressViewer - модуль визуализации длительных операций }
{ (c) 2005 - 2008 Логинов Дмитрий Сергеевич }
{ Последнее обновление: 27.01.2008 }
{ Адрес сайта: http://matrix.kladovka.net.ru/ }
{ e-mail: loginov_d@inbox.ru }
{ }
{ Свои предложения по доработке модуля высылайте на указанный электронный }
{ ящик, либо пишите в гостевой книге сайта. Любые изменения в модуле должны }
{ быть согласованы с его автором. }
{ }
{ *************************************************************************** }
unit ProgressViewer;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, SyncObjs;
{
Класс-поток TProgressViewer выполняет визуализацию хода выполнения длительного
процесса, запущенного в другом (в частности в основном) потоке.
В начале процедуры Execute на API создается новое окошко, в котором собственно
и выполняется необходимая отрисовка (используется буфер TBitmap).
Для завершения работы процесса следует использовать метод Terminate.
Примеры использования
1) При визуализации хода процесса неопределенной длительности:
with TProgressViewer.Create('Визуализация без процентов', False) do
try
Sleep(10000); // Здесь выполняется длительная операция
finally
Terminate;
end;
2) При визуализации хода процесса с индикацией процента выполнения:
with TProgressViewer.Create('Визуализация с процентами', True) do
try
for I := 1 to 1000 do
begin
CurrentValue := I / 10;
if CancelByUser then Break;
//CheckCancelByUser;
Sleep(1); // Задержка, чтобы окно не исчезло сразу же
end;
finally
Terminate;
end;
}
{
16.12.2007
- исправлена ошибка, связанная с крахом отрисовки всей графики
приложения, если во время визуализации окна програсса происходит обновление
любой VCL-формы. Пришлось добавить методы LockCanvas и UnlockCanvas,
которые соответственно блокируют и разблокируют канву для всех используемых
в дополнительном потоке объектов TBitmap. Блокировку канвы в дополнительном
потоке необходимо делать при изменении любых ее свойств. Теперь утечки
GDI-ресурсов не возникает.
- добавлена кнопка "Отмена" и обработка клавиши "Escape". См. метод CancelByUser
- внедрен механизм вывода любой текстовой информации в окно прогресса. Для
этого следует использовать функцию AddStringInfo(). Для нее указываются номер строки,
определяющий порядок вывода текстовых сообщений, текст сообщение, а также
дополнительные аргументы, с помощью которых можно изменить любые свойства
шрифта. По умолчанию сообщение с номером -1 определяет название огранизации,
а сообщение с номером 0 - имя текущей операции.
17.12.2007
- добавлен метод SetWndWidth, позволяющий изменить размер окна визуализации.
Работает в асинхронном режиме, т.е. не посылает сообщение на изменение размеров,
а уведомляет основной цикл потока о том, что при очередной итерации
требуется изменить размер окна.
25.12.2007
- добавлена функция RegisterProgressWndClass, выполняющая регистрацию класса
окна при старте программы.
- исправлен метод SetCancelBtnVisible. Теперь в нем нет вызовов SendMessage,
а просто-напросто устанавливается флаг FNewBtnVisible, который учитывается в
функции CheckWidthAndVisible. В какой бы момент пользователь не изменил видимость
кнопки, программа обязательно на это в свое время отреагирует.
- теперь для отрисовки изображения вместо WM_PAINT посылается пользовательское
сообщение WM_DOPAINT. Разницы нет никакой, зато это меньше будет путать программистов.
- создание и удаление объектов TBitmap перенесено туда, где и положено -
в конструктор и деструктор.
26.12.2007
- добавлен метод ChangeStringInfo(), позволяющий изменить произвольный
параметр текстовой строки.
27.12.2007
- добавлены переменные ProgressWaitLabel1 и ProgressWaitLabel2, определяющие
текстовые сообщение, которые всегда выводятся над строкой прогресса. Теперь
эти строки можно отключить либо изменить любые их атрибуты. Их индексы:
1001 и 1002 соответственно.
- теперь в целях локализации можно поменять любой текст.
- сообщения об ошибках перенесены в секцию RESOURCESTRING
- добавлен перегруженный метод AddStringInfo(), в который вместо списка
параметров передается запись TStringInfo.
27.01.2008
- добавлены возможность задания значений по умолчанию для шрифта (цвет, размер,
имя, стиль) (со шрифтом есть проблема неоднозначности, решаемая с помощью
директивы CanUseDefaultFontStyle.).
- исправлена ошибка, связанная с неправильной прорисовкой текста. Границы
прямоугольника сужалить с краев на 10 пикселей 2 раза вместо одного. В результате
текст мог обрезаться (даже при переносе строк).
- добавлен свой класс исключения - EProgressException
- добавлен метод CheckCancelByUser, генерирующий исключение EProgressCancelByUser
в случае отмены операции пользователем. В ходе выполнения длительной операции
достаточно периодически вызывать данный метод и он прервет при необходимости
операцию благодаря генерации исключения.
}
{ Разрешает генерацию исключения в наиболее опасных местах кода. По-умолчанию
выключено. Включать не рекомендую, но если очень хочется, то я не против}
{.$DEFINE CanRaiseException}
{ Разрешает использовать стиль шрифта по умолчанию. Если при вызове функции
AddStringInfo() аргумент AFontStyle не указывается, или равен
[fsBold, fsItalic, fsUnderline, fsStrikeOut], то берется значение по умолчанию,
т.е. FDefaultFontStyle или GlobalDefaultFontStyle. В случае же, если данная
опция отключена, то значение, что указывается при вызове функции
AddStringInfo(), будет использоваться }
{$DEFINE CanUseDefaultFontStyle}
const
{$IFDEF CanUseDefaultFontStyle}
FontStyleNone = [fsBold, fsItalic, fsUnderline, fsStrikeOut];
{$ELSE}
FontStyleNone = [];
{$ENDIF}
type
TProgressViewer = class;
TAdditionalThread = class;
EProgressException = class(Exception);
EProgressCancelByUser = class(EProgressException);
TProgressMethod = procedure(AThread: TProgressViewer) of Object;
TStringInfo = record
sLineNumber: Integer; // Номер строки
sText: string; // Текст строки
sAlignment: TAlignment; // Выравнивание
sLineBreak: Boolean; // Перенос строк
sFontName: TFontName; // Имя шрифта
sFontSize: Integer; // Размер шрифта
sFontStyle: TFontStyles; // Стиль шрифта
sFontColor: TColor; // Цвет шрифта
sLineHeight: Integer; // Высота строки. Временное поле
end;
TStringInfoParams = (sipText, sipAlignment, sipLineBreak, sipFontName,
sipFontSize, sipFontStyles, sipFontColor);
TProgressViewer = class(TThread)
private
FCurrentValue: Double;
FShowAsPercentBar: Boolean;
FFormLeftTop: TPoint;
FBitmap: TBitmap;
FFonBitmap: TBitmap;
FTextBitmap: TBitmap;
FBMPLine: TBitmap;
FhWindow: HWND;
FhButton: HWND;
FForegroundWindow: HWND;
FStartTime: TDateTime;
FAdditionalThread: TAdditionalThread;
FStringInfoCS: TCriticalSection;
FDataCS: TCriticalSection;
FStringInfo: array of TStringInfo;
FBufferIsReady: Boolean;
FWndWidth: Integer;
FWndHeight: Integer;
FNewWidth: Integer; // Новое значение ширины экрана
FNewBtnVisible: Boolean;
FCancelByUser: Boolean;
FCancelBtnVisible: Boolean;
FBGColor: TColor;
FText: string;
FOnCancelByUser: TNotifyEvent;
FDefaultFontColor: TColor;
FDefaultFontSize: Integer;
FDefaultFontStyle: TFontStyles;
FDefaultFontName: string;
procedure DrawProgress;
{ Создает API-шкое окошко с кнопочкой }
procedure CreateNewWnd;
{ Возвращает индекс элемента в массиве FStringInfo по sLineNumber }
function GetLineNumberIndex(LineNumber: Integer): Integer;
procedure SetText(const Value: string);
{ Блокирует канву всех используемых объектов TBitmap }
procedure LockCanvas;
{ Снимает блокировку канвы всех используемых объектов TBitmap }
procedure UnLockCanvas;
{ Регистрирует созданное окно и поток в списке }
procedure RegisterWnd;
{ Удаляет информацию о потоке и окне из списка регистрации }
procedure UnRegisterWnd;
procedure SetCancelBtnVisible(const Value: Boolean);
{ Устанавливает новые размеры окна визуализации. }
procedure SetWndSize(NewWidth: Integer; NewHeight: Integer = -1);
{ Выполняет сортировку массива FStringInfo }
procedure SortStringInfoArray;
procedure SetOnCancelByUser(const Value: TNotifyEvent);
procedure DoCancelByUser;
public
{ Создает объект визуализации.
- AText - имя выполняемой операции
- ShowAsPercentBar - режим отрисовки строки прогресса
- CancelBtnVisible - определяет, следует ли отображать кнопку "Отмена"
- ProgressMethod - позволяет выполнить обработку данных в доп. потоке }
constructor Create(AText: string; ShowAsPercentBar: Boolean = False;
CancelBtnVisible: Boolean = False; ProgressMethod: TProgressMethod = nil);
{ Удаляет объекты, созданные в конструкторе. Не вызывайте метод Free или
Destroy напрямую. Гораздо быстрее вызвать метод Terminate и не дожидаться,
когда объект уничтожится полностью. }
destructor Destroy; override;
{ Завершает прогресс и ожидает, пока не уничтожится окно визуализации.
После этого возвращает ActiveWindow }
procedure TerminateProgress;
{ Текущее значение прогресса. Если в конструкторе ShowAsPercentBar=True,
то строка прогресса отображает текущее значение. В противном случае
в ней слева-направо непрерывно перемещается прямоугольник }
property CurrentValue: Double read FCurrentValue write FCurrentValue;
{ Основной текст окна визуализации. Т.е. операция, которая выполняется
в данный момент }
property Text: string read FText write SetText;
{ Задает цвет шрифта по умолчанию }
property DefaultFontColor: TColor read FDefaultFontColor write FDefaultFontColor;
{ Размер шрифта по умолчанию }
property DefaultFontSize: Integer read FDefaultFontSize write FDefaultFontSize;
{ Стиль шрифта по умолчанию }
property DefaultFontStyle: TFontStyles read FDefaultFontStyle write FDefaultFontStyle;
{ Имя шрифта по умолчанию }
property DefaultFontName: string read FDefaultFontName write FDefaultFontName;
{ Добавляет текстовую строку с указанным номером. Строки с номерами
0 и -1 зарезервированы и добавляются автоматически при создании объекта }
procedure AddStringInfo(LineNumber: Integer; AText: string;
TextAlignment: TAlignment = taRightJustify; ALineBreak: Boolean = False;
AFontColor: TColor = clDefault; AFontStyle: TFontStyles = FontStyleNone;
AFontSize: Integer = -1; AFontName: string = ''); overload;
procedure AddStringInfo(LineNumber: Integer; StringInfo: TStringInfo); overload;
{ Позволяет получить информацию о строке с заданным номером }
function GetStringInfo(LineNumber: Integer): TStringInfo;
{ Позволяет изменить произвольный параметр указанной текстовой строки.
Массив параметров и массив значений должны заполняться синхронно.
Для передачи стиля шрифта необходимо его сперва преобразовать в байт с
помощью функции FontStylesToInt(). Если нет строки с указанным номером,
то будет сгенерировано исключение. }
procedure ChangeStringInfo(LineNumber: Integer;
StringParams: array of TStringInfoParams; Values: array of Variant);
{ Удаляет из FStringInfo элемент с номером LineNumber }
procedure DeleteStringInfo(LineNumber: Integer);
{ При нажатии на кнопку "Отмена" или клавишу "Escape" эта переменная
устанавливается в True. Цикл обработки периодически проверяет
данный флаг, и если он = True, по цикл прерывается и вызывается Terminate}
property CancelByUser: Boolean read FCancelByUser write FCancelByUser;
{ Генерирует исключение "Прервано пользователем", если выставлен флаг FCancelByUser }
procedure CheckCancelByUser;
{ Включает или выключает видимость кнопки "Отмена". Обработка клавиши
"Escape" работает в любом случае, даже если кнопка "Отмена" невидима}
property CancelBtnVisible: Boolean read FCancelBtnVisible write SetCancelBtnVisible;
{ Цвет фона окна прогресса }
property BGColor: TColor read FBGColor write FBGColor;
{ Позволяет изменить размер окна визуализации. Работает в асинхронном режиме,
т.е. не посылает сообщение на изменение размеров, а уведомляет основной
цикл потока о том, что при очередной итерации требуется изменить размер окна }
procedure SetWndWidth(NewWidth: Integer);
{ Данный обработчик вызывается при нажатии пользователем кнопки "Отмена" или "Esc".
Помните, что данный метод вызывается из дополнительного потока!}
property OnCancelByUser: TNotifyEvent read FOnCancelByUser write SetOnCancelByUser;
protected
procedure Execute; override;
end;
TAdditionalThread = class(TThread)
private
FOwnerThread: TProgressViewer;
FProgressMethod: TProgressMethod;
protected
procedure Execute; override;
end;
{ Конвертирует стиль шрифта в Integer для возможности использования в
функции ChangeStringParams }
function FontStylesToInt(AFontStyles: TFontStyles): Integer;
var
SProductName: string = 'Ваше приложение';
DrawPercentLabel: Boolean = True;
SleepTime: Integer = 50;
DrawTimeLabel: Boolean = True;
ProgressColor: TColor = clAqua;
CancelButtonText: string = 'Отмена (Esc)';
CancelByUserMessage: string = 'Прервано пользователем!';
ProgressWaitLabel1: string = 'Выполняется длительная операция. Она может';
ProgressWaitLabel2: string = 'продлиться несколько минут. Пожалуйста, подождите!';
ProgressCommonTime: string = 'Общее время: %s';
GlobalDefaultFontName: string = 'MS Sans Serif'; // Имя шрифта по умолчанию
GlobalDefaultFontColor: TColor = clBlack; // Цвет шрифта по умолчанию
GlobalDefaultFontSize: Integer = 8; // Размер шрифта по умолчанию
GlobalDefaultFontStyle: TFontStyles = []; // Стиль шрифта по умолчанию
const
{ Индекс строки, по которому записывается свойство Text }
TEXT_INDEX = 0;
WAIT_TEXT_INDEX = 1000;
implementation
const
ProgressWindowClassName = 'ProgressViewerFormClass';
DefWndWidth = 450;
DefWndHeight = 70; // Минимальная высота окна (без единой текстовой строки)
DefBGColor = clBtnFace;
WM_DOPAINT = WM_USER + 1;
resourcestring
ErrorRegWndMsg = 'Класс окна визуализации "' + ProgressWindowClassName + '" не зарегистрирован!';
ErrorStringNotFound = 'Строка с номером %d не найдена!';
ErrorWrongDimension = 'Количество параметров и соответствующих им значений не совпадают!';
var
{ Синхронные списки, позволяющие по Handle окна определить породивший
его объект TProgressViewer }
WndHandleList: TThreadList;
ThreadsList: TList;
ClearStringInfo: TStringInfo; // Запись с нулевыми полями
WndClassIsReg: Boolean;
function FontStylesToInt(AFontStyles: TFontStyles): Integer;
begin
Result := Byte(AFontStyles);
end;
{ Для указанного окна отыскивает породивший его поток }
function FindProgressViewerForWindow(AWndHandle: HWND): TProgressViewer;
var
Index: Integer;
begin
Result := nil;
with WndHandleList.LockList do
try
Index := IndexOf(Pointer(AWndHandle));
if Index >= 0 then
Result := ThreadsList[Index];
finally
WndHandleList.UnlockList;
end;
end;
function MainWndProc(hWindow: HWND; Msg: UINT; wParam: wParam;
lParam: lParam): LRESULT; stdcall;
var
ps: TPaintStruct;
pw: TProgressViewer;
begin
Result := 0;
pw := nil;
if (Msg = WM_DOPAINT) or (Msg = WM_COMMAND) or (Msg = WM_KEYDOWN) then
pw := FindProgressViewerForWindow(hWindow);
case Msg of
WM_DOPAINT:
// В параметре wParam должна находиться ссылка на объект TBitmap,
// в котором уже хранится подготовленное изображение
if wParam <> 0 then
begin
BeginPaint(hWindow, ps);
with TCanvas.Create do
try
Lock;
Handle := GetDC(hWindow);
Draw(0, 0, TProgressViewer(wParam).FBitmap); // Ошибка может возникнуть только здесь (хотя вряд-ли)
finally
ReleaseDC(hWindow, Handle);
UnLock;
Free;
EndPaint(hWindow, ps);
end;
end else
Result := DefWindowProc(hWindow, Msg, wParam, lParam);
WM_COMMAND:
if lParam <> 0 then
if pw <> nil then
pw.DoCancelByUser;
WM_KEYDOWN:
if wParam = VK_ESCAPE then
if pw <> nil then
pw.DoCancelByUser;
WM_DESTROY: PostQuitMessage(0);
WM_CLOSE:; // Запрещаем закрытие окна визуализации на Alt+F4
else
Result := DefWindowProc(hWindow, Msg, wParam, lParam);
end;
end;
{ TProgressViewer }
procedure TProgressViewer.AddStringInfo(LineNumber: Integer; AText: string;
TextAlignment: TAlignment = taRightJustify; ALineBreak: Boolean = False;
AFontColor: TColor = clDefault; AFontStyle: TFontStyles = FontStyleNone;
AFontSize: Integer = -1; AFontName: string = '');
var
Index: Integer;
begin
FStringInfoCS.Enter;
try
Index := GetLineNumberIndex(LineNumber);
if Index < 0 then
begin
Index := Length(FStringInfo);
SetLength(FStringInfo, Index + 1);
end;
if AFontColor = clDefault then
if FDefaultFontColor = clDefault then
AFontColor := GlobalDefaultFontColor
else
AFontColor := FDefaultFontColor;
if AFontSize = -1 then
if FDefaultFontSize = -1 then
AFontSize := GlobalDefaultFontSize
else
AFontSize := FDefaultFontSize;
{$IFDEF CanUseDefaultFontStyle}
if AFontStyle = FontStyleNone then
if FDefaultFontStyle = FontStyleNone then
AFontStyle := GlobalDefaultFontStyle
else
AFontStyle := FDefaultFontStyle;
{$ENDIF}
if AFontName = '' then
if FDefaultFontName = '' then
AFontName := GlobalDefaultFontName
else
AFontName := FDefaultFontName;
with FStringInfo[Index] do
begin
sLineNumber := LineNumber;
sText := AText;
sAlignment := TextAlignment;
sLineBreak := ALineBreak;
sFontName := AFontName;
sFontSize := AFontSize;
sFontStyle := AFontStyle;
sFontColor := AFontColor;
end;
// Осуществляем сортировку массива
SortStringInfoArray;
FBufferIsReady := False;
finally
FStringInfoCS.Leave;
end;
end;
procedure TProgressViewer.AddStringInfo(LineNumber: Integer;
StringInfo: TStringInfo);
begin
AddStringInfo(LineNumber, StringInfo.sText, StringInfo.sAlignment,
StringInfo.sLineBreak, StringInfo.sFontColor, StringInfo.sFontStyle,
StringInfo.sFontSize, StringInfo.sFontName);
end;
procedure TProgressViewer.ChangeStringInfo(LineNumber: Integer;
StringParams: array of TStringInfoParams; Values: array of Variant);
var
I, Index: Integer;
StringInfo: TStringInfo;
begin
FStringInfoCS.Enter;
try
try
Index := GetLineNumberIndex(LineNumber);
if Index < 0 then
raise EProgressException.CreateFmt(ErrorStringNotFound, [Index])
else
StringInfo := FStringInfo[Index];
if Length(StringParams) <> Length(Values) then
raise EProgressException.Create(ErrorWrongDimension);
for I := 0 to High(StringParams) do
case StringParams[I] of
sipText: StringInfo.sText := Values[I];
sipAlignment: StringInfo.sAlignment := Values[I];
sipLineBreak: StringInfo.sLineBreak := Values[I];
sipFontName: StringInfo.sFontName := Values[I];
sipFontSize: StringInfo.sFontSize := Values[I];
sipFontStyles: StringInfo.sFontStyle := TFontStyles(Byte(Values[I]));
sipFontColor: StringInfo.sFontColor := Values[I];
end;
FStringInfo[Index] := StringInfo;
except
on E: Exception do
raise EProgressException.Create('TProgressViewer.ChangeStringParams -> ' + E.Message);
end;
FBufferIsReady := False;
finally
FStringInfoCS.Leave;
end;
end;
procedure TProgressViewer.CheckCancelByUser;
begin
if FCancelByUser then
raise EProgressCancelByUser.Create(CancelByUserMessage);
end;
constructor TProgressViewer.Create(AText: string; ShowAsPercentBar: Boolean = False;
CancelBtnVisible: Boolean = False; ProgressMethod: TProgressMethod = nil);
var
ScreenRect: TRect;
begin
if not WndClassIsReg then
raise EProgressException.Create(ErrorRegWndMsg);
inherited Create(False);
FForegroundWindow := GetForegroundWindow;
FStringInfoCS := TCriticalSection.Create;
FDataCS := TCriticalSection.Create;
FBitmap := TBitmap.Create;
FFonBitmap := TBitmap.Create;
FTextBitmap := TBitmap.Create;
FBMPLine := TBitmap.Create;
FShowAsPercentBar := ShowAsPercentBar;
FWndWidth := DefWndWidth;
FNewWidth := DefWndWidth;
FWndHeight := DefWndHeight;
FCancelBtnVisible := CancelBtnVisible;
FNewBtnVisible := CancelBtnVisible;
FBGColor := DefBGColor;
FreeOnTerminate := True;
SystemParametersInfo(SPI_GETWORKAREA, 0, @ScreenRect, 0);
FFormLeftTop.X := (ScreenRect.Right - ScreenRect.Left - FWndWidth) div 2;
FFormLeftTop.Y := ScreenRect.Bottom - FWndHeight - 100;
FDefaultFontColor := clDefault;
FDefaultFontSize := -1;
FDefaultFontStyle := FontStyleNone;
if AText = '' then AText := ' ';
Text := AText;
{ Включаем название организации }
AddStringInfo(-1, SProductName, taCenter, False, clBlack, [fsBold], 12, GlobalDefaultFontName);
{ Включаем метки с текстом "Пожалуйста, подождите!" }
AddStringInfo(WAIT_TEXT_INDEX, ' ', taCenter, False, clBlack, [fsBold], 8);
AddStringInfo(WAIT_TEXT_INDEX + 1, ProgressWaitLabel1, taCenter, False, clBlack, [fsBold], 10, GlobalDefaultFontName);
AddStringInfo(WAIT_TEXT_INDEX + 2, ProgressWaitLabel2, taCenter, False, clBlack, [fsBold], 10, GlobalDefaultFontName);
if Assigned(ProgressMethod) then
begin
FAdditionalThread := TAdditionalThread.Create(True);
FAdditionalThread.FProgressMethod := ProgressMethod;
FAdditionalThread.FOwnerThread := Self;
FAdditionalThread.FreeOnTerminate := True;
end;
end;
procedure TProgressViewer.CreateNewWnd;
var
WndHandle: HWND;
begin
WndHandle := CreateWindowEx(
WS_EX_TOOLWINDOW or WS_EX_TOPMOST, // Чтобы не было кнопки на панели задач
ProgressWindowClassName,
'ProgressWnd',
WS_VISIBLE or WS_POPUP,
FFormLeftTop.X, FFormLeftTop.Y,
FWndWidth, FWndHeight,
0,
0,
hInstance, // Адрес модуля, предоставляющего оконную функцию
nil);
// Дальнейшие действия следует выполнять только при успешном создании окна
// Если по какой-то причине окно создать не удалось, то приложение все-равно
// не свалится - просто не будет никакой отрисовки.
if WndHandle <> 0 then
begin
// Показ окна на экране
ShowWindow(WndHandle, SW_SHOW);
FhWindow := WndHandle; // Запоминает дескриптор созданного окна
RegisterWnd; // Регистрируем созданное окно
// Создаем кнопку "Отмена"
FhButton := CreateWindow ('BUTTON', PChar(CancelButtonText), WS_CHILD,
FWndWidth - 120, FWndHeight - 30, 100, 25, WndHandle, 0, hInstance, nil );
if FhButton <> 0 then
begin
if FCancelBtnVisible then
begin
ShowWindow(FhButton, SW_SHOW);
UpdateWindow(FhButton);
end;
end;
end;
end;
procedure TProgressViewer.DeleteStringInfo(LineNumber: Integer);
var
Index, I: Integer;
begin
FStringInfoCS.Enter;
try
Index := GetLineNumberIndex(LineNumber);
if Index >= 0 then
begin
if Index < High(FStringInfo) then
for I := Index + 1 to High(FStringInfo) do
FStringInfo[I - 1] := FStringInfo[I];
SetLength(FStringInfo, High(FStringInfo));
FBufferIsReady := False;
end;
finally
FStringInfoCS.Leave;
end;
end;
destructor TProgressViewer.Destroy;
begin
FreeOnTerminate := False;
inherited Destroy;
FStringInfoCS.Free;
FBitmap.Free;
FFonBitmap.Free;
FTextBitmap.Free;
FBMPLine.Free;
FDataCS.Free;
end;
procedure TProgressViewer.DoCancelByUser;
begin
FCancelByUser := True;
if Assigned(FOnCancelByUser) then
try
FOnCancelByUser(Self);
except
{$IFDEF CanRaiseException}raise;{$ENDIF}
end;
end;
procedure TProgressViewer.DrawProgress;
var
S: string;
TxtWidth: Integer;
begin
LockCanvas;
try
FBitmap.Assign(FFonBitmap);
FBMPLine.FreeImage;
FBMPLine.Width := FBitmap.Width - 40;
FBMPLine.Height := 20;
FBMPLine.Canvas.Brush.Style := bsSolid;
FBMPLine.Canvas.Brush.Color := clWhite;
FBMPLine.Canvas.Rectangle(FBMPLine.Canvas.ClipRect);
FBMPLine.Canvas.Brush.Color := ProgressColor;
if not FShowAsPercentBar then
begin
FCurrentValue := FCurrentValue + 10;
if FCurrentValue > FBMPLine.Width then
FCurrentValue := -40;
FBMPLine.Canvas.Rectangle(Trunc(FCurrentValue), 2,
Trunc(FCurrentValue) + 40, FBMPLine.Height - 2);
end else
begin
if FCurrentValue > 100 then FCurrentValue := 100 else
if FCurrentValue < 0 then FCurrentValue := 0;
FBMPLine.Canvas.Rectangle(2, 2, 2 + Trunc(FCurrentValue *
(FBMPLine.Width - 4) / 100), FBMPLine.Height - 2);
// Выводим проценты
if DrawPercentLabel then
begin
S := IntToStr(Trunc(FCurrentValue)) + ' %';
TxtWidth := FBMPLine.Canvas.TextWidth(S);
FBMPLine.Canvas.Brush.Style := bsClear;
FBMPLine.Canvas.Font.Style := [fsBold];
FBMPLine.Canvas.TextOut((FBMPLine.Width - TxtWidth) div 2, 4, S);
end;
end;
if DrawTimeLabel then
begin
S := Format(ProgressCommonTime, [TimeToStr(Time - FStartTime)]);
FBitmap.Canvas.Font.Style := [];
FBitmap.Canvas.Brush.Style := bsClear;
TxtWidth := FBitmap.Canvas.TextWidth(S);
if FCancelBtnVisible then
FBitmap.Canvas.TextOut(20, FWndHeight - 25, S)
else
FBitmap.Canvas.TextOut(FWndWidth - TxtWidth - 20, FWndHeight - 25, S);
end;
Windows.BitBlt(FBitmap.Canvas.Handle, 20, FWndHeight - 60, FBMPLine.Width, FBMPLine.Height,
FBMPLine.Canvas.Handle, 0, 0, SRCCOPY);
finally
SendMessage(FhWindow, WM_DOPAINT, Integer(Self), 0);
UnLockCanvas;
end;
end;
procedure TProgressViewer.Execute;
var
Msg: TMsg;
procedure DrawText;
begin
LockCanvas;
try
FBitmap.FreeImage;
FFonBitmap.FreeImage;
FBitmap.Width := FWndWidth;
FBitmap.Height := FWndHeight;
FBitmap.Canvas.Brush.Color := clBtnFace;
FBitmap.Canvas.Rectangle(FBitmap.Canvas.ClipRect);
FFonBitmap.Assign(FBitmap);
if FTextBitmap.Height > 0 then
FFonBitmap.Canvas.Draw(10, 2, FTextBitmap);
finally
UnLockCanvas;
end;
end;
// Рисует текст из массива FStringInfo в буфере FTextBitmap
procedure PrepareTextRegion;
var
tbWidth, tbHeight, I, tmp: Integer;
ARect: TRect;
AlignValue: Cardinal;
WordBreak: Cardinal;
begin
LockCanvas;
FStringInfoCS.Enter;
try
tbWidth := FWndWidth - 20;
// Определяем высоту блока текстовых надписей
tbHeight := 0;
FTextBitmap.Height := 0;
for I := 0 to High(FStringInfo) do
begin
with FTextBitmap.Canvas, FStringInfo[I] do
begin
Font.Name := sFontName;
Font.Size := sFontSize;
Font.Style := sFontStyle;
ARect.Left := 0;
ARect.Top := 0;
ARect.Right := tbWidth;
ARect.Bottom := 0;
if sLineBreak then
WordBreak := DT_WORDBREAK
else
WordBreak := 0;
Windows.DrawText(Handle, PChar(sText), Length(sText), ARect,
DT_CALCRECT or WordBreak);
tmp := TextWidth(sText);
inttostr(tmp);
sLineHeight := ARect.Bottom;
tbHeight := tbHeight + sLineHeight;
end;
end;
FTextBitmap.Width := tbWidth;
FTextBitmap.Height := tbHeight;
FTextBitmap.Canvas.Brush.Color := FBGColor;
FTextBitmap.Canvas.FillRect(FTextBitmap.Canvas.ClipRect);
// Выполняем отрисовку на FTextBitmap
tbHeight := 0;
for I := 0 to High(FStringInfo) do
begin
with FTextBitmap.Canvas, FStringInfo[I] do
begin
Font.Name := sFontName;
Font.Size := sFontSize;
Font.Style := sFontStyle;
Font.Color := sFontColor;
ARect.Left := 0;
ARect.Top := tbHeight;
ARect.Right := tbWidth;
ARect.Bottom := tbHeight + sLineHeight;
case sAlignment of
taLeftJustify: AlignValue := DT_LEFT;
taRightJustify: AlignValue := DT_RIGHT;
else
AlignValue := DT_CENTER;
end;
if sLineBreak then
WordBreak := DT_WORDBREAK
else
WordBreak := 0;
Windows.DrawText(Handle, PChar(sText), Length(sText), ARect,
AlignValue or WordBreak);
tbHeight := tbHeight + sLineHeight;
if sLineNumber = 0 then
tbHeight := tbHeight;
end;
end;
// Отрисовываем на FBitmap
if FTextBitmap.Height > 0 then
SetWndSize(-1, DefWndHeight + FTextBitmap.Height);
finally
UnLockCanvas;
FStringInfoCS.Leave;
end;
end;
procedure PrepareImageForWindow;
begin
PrepareTextRegion;
DrawText;
end;
procedure ProcessMessages;
begin
if FhWindow <> 0 then
while PeekMessage(Msg, FhWindow, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end;
procedure UpdateButton;
begin
if FhButton <> 0 then
begin
InvalidateRect(FhButton, nil, False);
ProcessMessages;
end;
end;
procedure CheckWidthAndVisible;
begin
FDataCS.Enter;
try
if FNewWidth <> FWndWidth then
SetWndSize(FNewWidth, -1);
// Попутно проверяем видимость кнопки "Отмена"
if FNewBtnVisible <> FCancelBtnVisible then
begin
if FhButton <> 0 then
begin
if FCancelBtnVisible then
ShowWindow(FhButton, SW_SHOW)
else
ShowWindow(FhButton, SW_HIDE);
end;
FCancelBtnVisible := FNewBtnVisible;
end;
finally
FDataCS.Leave;
end;
end;
begin
// Обрабатываем случай, когда перед созданием окна пользователь успел
// вызвать метод SetWndWidth
CheckWidthAndVisible;
// Сооздаем в данном потоке новое окошко, в котором и будем выполнять отрисовку
CreateNewWnd;
// Запоминаем время старта
FStartTime := Time;
if Assigned(FAdditionalThread) then
FAdditionalThread.Resume;
while not Terminated do
begin
// Если пользователь изменил значение ширины окна или видимость кнопки
// "отмена", то обрабатываем это
CheckWidthAndVisible;
// Реагируем на возможную смену ширины окна
ProcessMessages;
// Подготавливаем изображение
FStringInfoCS.Enter;
try
if not FBufferIsReady then
begin
try
PrepareImageForWindow;
except
{$IFDEF CanRaiseException}raise;{$ENDIF}
end;
FBufferIsReady := True;
end;
finally
FStringInfoCS.Leave;
end;
if FhWindow <> 0 then
try
DrawProgress;
except
{$IFDEF CanRaiseException}raise;{$ENDIF}
end;
// Выполняем перерисовку кнопки
UpdateButton;
Sleep(SleepTime);
end;
if Assigned(FAdditionalThread) then
FAdditionalThread.Terminate;
// Прячем окошко, после чего дестроим его
if FhWindow <> 0 then
begin
ShowWindow(FhWindow, SW_HIDE);
SendMessage(FhWindow, WM_DESTROY, 0, 0);
UnRegisterWnd;
//if FForegroundWindow <> 0 then
// SetForegroundWindow(FForegroundWindow);
//FForegroundWindow := 0;
FhWindow := 0;
end;
end;
function TProgressViewer.GetLineNumberIndex(LineNumber: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to High(FStringInfo) do
if FStringInfo[I].sLineNumber = LineNumber then
begin
Result := I;
Exit;
end;
end;
function TProgressViewer.GetStringInfo(LineNumber: Integer): TStringInfo;
var
Index: Integer;
begin
FStringInfoCS.Enter;
try
Result := ClearStringInfo;
Index := GetLineNumberIndex(LineNumber);
if Index >= 0 then
Result := FStringInfo[Index];
finally
FStringInfoCS.Leave;
end;
end;
procedure TProgressViewer.LockCanvas;
begin
FBitmap.Canvas.Lock;
FFonBitmap.Canvas.Lock;
FTextBitmap.Canvas.Lock;
FBMPLine.Canvas.Lock;
end;
procedure TProgressViewer.RegisterWnd;
begin
if FhWindow <> 0 then
with WndHandleList.LockList do
try
if IndexOf(Pointer(FhWindow)) < 0 then
begin
Add(Pointer(FhWindow));
ThreadsList.Add(Self);
end;
finally
WndHandleList.UnlockList;
end;
end;
procedure TProgressViewer.SetCancelBtnVisible(const Value: Boolean);
begin
FDataCS.Enter;
FNewBtnVisible := Value;
FDataCS.Leave;
end;
procedure TProgressViewer.SetOnCancelByUser(const Value: TNotifyEvent);
begin
FOnCancelByUser := Value;
end;
procedure TProgressViewer.SetText(const Value: string);
begin
FText := Value;
if GetLineNumberIndex(TEXT_INDEX) < 0 then
AddStringInfo(TEXT_INDEX, Value, taCenter, False, clBlack, [fsBold], 14)
else
ChangeStringInfo(TEXT_INDEX, [sipText], [Value]);
end;
procedure TProgressViewer.SetWndSize(NewWidth, NewHeight: Integer);
var
ScreenRect: TRect;
begin
if NewHeight = -1 then NewHeight := FWndHeight;
if NewWidth = -1 then NewWidth := FWndWidth;
if NewHeight < DefWndHeight then
NewHeight := DefWndHeight;
if NewWidth < DefWndWidth div 2 then
NewWidth := DefWndWidth div 2;
if (FWndHeight <> NewHeight) or (FWndWidth <> NewWidth) then
begin
SystemParametersInfo(SPI_GETWORKAREA, 0, @ScreenRect, 0);
FWndHeight := NewHeight;
FWndWidth := NewWidth;
FFormLeftTop.X := (ScreenRect.Right - ScreenRect.Left - FWndWidth) div 2;
FFormLeftTop.Y := ScreenRect.Bottom - FWndHeight - 100;
if FhWindow <> 0 then
SetWindowPos(FhWindow, HWND_TOPMOST, FFormLeftTop.X, FFormLeftTop.Y, FWndWidth,
FWndHeight, SWP_SHOWWINDOW);
if FhButton <> 0 then
begin
SetWindowPos(FhButton, HWND_TOP, FWndWidth - 120, FWndHeight - 30,
100, 25, SWP_HIDEWINDOW);
if FCancelBtnVisible then
begin
ShowWindow(FhButton, SW_SHOW);
UpdateWindow(FhButton);
end;
end;
FBufferIsReady := False;
end;
end;
procedure TProgressViewer.SetWndWidth(NewWidth: Integer);
begin
if NewWidth < DefWndWidth div 2 then
NewWidth := DefWndWidth div 2;
FDataCS.Enter;
FNewWidth := NewWidth;
FDataCS.Leave;
end;
procedure TProgressViewer.SortStringInfoArray;
var
I, J: Integer;
AStringInfo: TStringInfo;
begin
for I := 0 to High(FStringInfo) - 1 do
for J := High(FStringInfo) downto I + 1 do
if FStringInfo[I].sLineNumber > FStringInfo[J].sLineNumber then
begin
AStringInfo := FStringInfo[I];
FStringInfo[I] := FStringInfo[J];
FStringInfo[J] := AStringInfo;
end;
end;
procedure TProgressViewer.TerminateProgress;
begin
Free;
if FForegroundWindow <> 0 then
SetForegroundWindow(FForegroundWindow);
end;
procedure TProgressViewer.UnLockCanvas;
begin
FBitmap.Canvas.Unlock;
FFonBitmap.Canvas.Unlock;
FTextBitmap.Canvas.Unlock;
FBMPLine.Canvas.Unlock;
end;
procedure TProgressViewer.UnRegisterWnd;
var
Index: Integer;
begin
with WndHandleList.LockList do
try
Index := IndexOf(Pointer(FhWindow));
if Index >= 0 then
begin
Delete(Index);
ThreadsList.Delete(Index);
end;
finally
WndHandleList.UnlockList;
end;
end;
{ TAdditionalThread }
procedure TAdditionalThread.Execute;
begin
FProgressMethod(FOwnerThread);
while not Terminated do
Sleep(50);
end;
procedure RegisterProgressWndClass;
var
WndClass: TWndClass;
begin
WndClass.lpszClassName := ProgressWindowClassName;
WndClass.lpfnWndProc := @MainWndProc;
WndClass.Style := CS_VREDRAW or CS_HREDRAW;
{Адрес модуля (EXE или DLL), в котором находится MainWndProc. В дальнейшем
функция CreateWindowEx в списке классов с одинаковым именем ProgressWindowClassName
благодаря hInstance сможет найти требуемый класс окна }
WndClass.hInstance := hInstance;
WndClass.hIcon := LoadIcon(0, IDI_APPLICATION);
WndClass.hCursor := LoadCursor(0, IDC_ARROW);
WndClass.hbrBackground := (COLOR_WINDOW + 1);
WndClass.lpszMenuName := nil;
WndClass.cbClsExtra := 0;
WndClass.cbWndExtra := 0;
WndClassIsReg := Windows.RegisterClass(WndClass) <> 0;
if not WndClassIsReg then
WndClassIsReg := GetLastError = ERROR_CLASS_ALREADY_EXISTS;
end;
initialization
WndHandleList := TThreadList.Create;
ThreadsList := TList.Create;
RegisterProgressWndClass;
finalization
WndHandleList.Free;
ThreadsList.Free;
end.