Delphi / GDI +: когда создается / уничтожается контекст устройства?

Обычно при использовании GDI + в Delphi вы можете использовать TPaintBox и рисовать во время события OnPaint:

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
   g: TGPGraphics;
begin
   g := TGPGraphics.Create(PaintBox1.Canvas.Handle);
   try
      g.DrawImage(FSomeImage, 0, 0);
   finally
      g.Free;
   end;
end;

Проблема с этой парадигмой заключается в том, что создание уничтожающего объекта Graphics каждый раз является расточительным и неэффективным. Кроме того, есть несколько конструкций, доступных в GDI +, которые вы можете используйте только тогда, когда у вас есть постоянный объект Graphics.

Проблема, конечно, в том, когда я могу создать этот объект Graphics? мне нужно знать, когда дескриптор станет доступным, а затем, когда он больше не действителен. мне нужна эта информация, чтобы я мог создать и уничтожить свой объект Graphics.


Попытка решения №1

я могу решить проблему создания, создав его, когда это действительно необходимо - при первом вызове цикла рисования:

procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
   if FGraphics = nil then
      FGraphics := TGPGraphics.Create(PaintBox1.Canvas.Handle);

   FGraphics.DrawImage(FSomeImage, 0, 0);
end;

Но я должен знать, когда контекст устройства больше не действителен, поэтому я могу уничтожить свой объект FGraphcis, чтобы он был воссоздан в следующий раз, когда это понадобится. Если по какой-то причине контекст устройства TPaintBox будет воссоздан, я буду использовать недопустимый контекст устройства при следующем вызове OnPaint.

Каков предполагаемый механизм в Delphi, чтобы я знал, когда создается, уничтожается или воссоздается дескриптор контекста устройства в TPaintBox?


person Ian Boyd    schedule 23.10.2009    source источник


Ответы (3)


Вы не можете использовать стандартный TPaintBox, потому что TPaintBox имеет Canvas типа TControlCanvas, члены которого имеют отношение к этой проблеме:

TControlCanvas = class(TCanvas)
private
  ...
  procedure SetControl(AControl: TControl);
protected
  procedure CreateHandle; override;
public
  procedure FreeHandle;
  ...
  property Control: TControl read FControl write SetControl;
end;

Проблема в том, что FreeHandle и SetControl не виртуальные.

Но: TControlCanvas создается и назначается здесь:

 constructor TGraphicControl.Create(AOwner: TComponent);
 begin
   inherited Create(AOwner);
   FCanvas := TControlCanvas.Create;
   TControlCanvas(FCanvas).Control := Self;
 end;

Итак, что вы могли бы сделать, так это создать нисходящий TMyControlCanvas, у которого есть виртуальные методы, и TMyPaintBox, который назначает Canvas следующим образом:

 constructor TMyPaintBox.Create(AOwner: TComponent);
 begin
   inherited Create(AOwner);
   FCanvas.Free;
   FCanvas := TMyControlCanvas.Create;
   TMyControlCanvas(FCanvas).Control := Self;
 end;

Затем вы можете использовать методы TMyControlCanvas для динамического создания и уничтожения вашего TGPGraphics.

Это должно вас подтолкнуть.

- Джерун

person Jeroen Wiert Pluimers    schedule 24.10.2009
comment
Есть ли проблемы с изменением размера элемента управления? Знаете ли вы, нужно ли вам воссоздавать контекст GDI + в этом случае или в каких-либо других обстоятельствах? - person David; 26.10.2009
comment
То, что вы добавляете виртуальные методы в TMyControlCanvas, не означает, что VCL будет их вызывать. - person Rob Kennedy; 26.10.2009
comment
@Rob На самом деле эти методы вызываются только из TControlCanvas, поэтому, если вы создадите свой собственный TControlCanvas, он будет работать. - person Jeroen Wiert Pluimers; 26.10.2009
comment
FreeHandle вызывается из автономной функции FreeDeviceContext. - person Rob Kennedy; 26.10.2009
comment
@Rob: вот почему TMyPaintBox теперь использует TMyControlCanvas (который может запускаться как тупая копия TControlCanvas). Это должно заставить VCL вызвать методы. Наличие виртуальных методов позволяет вам унаследовать другой класс от TMyControlCanvas, выполняющий более конкретные задачи (я больше отношусь к лагерю «большинство вещей должно быть виртуальным», но я могу понять, почему люди находятся в лагере «ограничения количества виртуальных методов» ). - person Jeroen Wiert Pluimers; 27.10.2009
comment
@ Дэвид: Я не уверен; простите :-) - person Jeroen Wiert Pluimers; 27.10.2009

Обнаружить создание легко. Просто переопределите CreateHandle в потомке TControlCanvas и поместите свой вместо по умолчанию один , как показывает ответ Джероена . Обнаружить разрушение сложнее.

Один из способов избежать этой проблемы - проверить, равен ли дескриптор TGpGraphics дескриптору рисования, поэтому вместо того, чтобы определять момент, когда контекст устройства освобожден, вы просто проверяете, прежде чем вам понадобится знать.

if not Assigned(FGraphics)
    or (FGraphics.GetHDC <> PaintBox1.Canvas.Handle) then begin
  FGraphics.Free;
  FGraphics := TGpGraphics.Create(PaintBox1.Canvas.Handle);
end;

Однако это, вероятно, ненадежно; значения дескрипторов могут использоваться повторно, поэтому, хотя значение HDC может быть одинаковым между двумя проверками, нет гарантии, что оно по-прежнему относится к одному и тому же объекту контекста устройства ОС.


Базовый класс TCanvas никогда не очищает собственное свойство Handle, поэтому все, что делает холст недействительным, должно происходить извне. TControlCanvas очищает свое свойство Handle, когда его свойство Control повторно назначается, но это обычно происходит только при создании элемента управления, поскольку TControlCanvas экземпляры редко используются совместно. Однако TControlCanvas экземпляры работают из пула дескрипторов контекста устройства, хранящегося в CanvasList. Когда одному из них нужен DC (в TControlCanvas.CreateHandle), он вызывает FreeDeviceContext, чтобы освободить место в кэше холста для дескриптора, который он собирается создать. Эта функция вызывает (не виртуальный) метод FreeHandle. Размер кеша равен 4 (см. CanvasListCacheSize), поэтому, если у вас есть несколько потомков TCustomControl или TGraphicControl в вашей программе, высока вероятность того, что вы получите промахи кеша всякий раз, когда необходимо перерисовать более четырех из них одновременно.

TControlCanvas.FreeHandle не является виртуальным и не вызывает никаких виртуальных методов . Хотя вы можете создать потомка этого класса и дать ему виртуальные методы, остальная часть VCL будет продолжать вызывать невиртуальные методы, не обращая внимания на какие-либо ваши дополнения.


Вместо того, чтобы пытаться определить, когда контекст устройства выпущен, вам может быть лучше использовать другой конструктор TGpGraphics. Используйте тот, который принимает дескриптор окна вместо дескриптора DC, например. Разрушение оконной ручки обнаружить намного легче. Для одноразового решения назначьте свой собственный метод для TPaintBox.WindowProc свойство и следите за сообщениями wm_Destroy. Если вы делаете это часто, создайте класс-потомок и переопределите _21 _ .

person Rob Kennedy    schedule 26.10.2009

Потеря производительности при создании / уничтожении графического объекта минимальна. Это намного перевешивает снижение производительности, связанное с использованием команд рисования gdi + в первую очередь. Когда дело доходит до рисования пользовательских интерфейсов, ни о чем из этого, я думаю, не стоит беспокоиться, потому что пользователь все равно ничего не заметит. И, честно говоря, может быть очень неудобно пытаться переносить графический объект и отслеживать изменения в дескрипторе DC (особенно если вы инкапсулируете графические подпрограммы внутри своего собственного набора классов).

Если вам нужно кэшировать растровые изображения, вы можете подумать о создании растрового изображения, которое вы хотите кэшировать с помощью GDI + (сделайте его подходящего размера и с любыми настройками сглаживания, которые вы хотите), сохраните его в tmemorystream, а затем, когда он вам понадобится , загрузите его из потока и нарисуйте, используя старый добрый bitblt. Это будет намного быстрее, чем при использовании Graphics.DrawImage. Я говорю на порядки быстрее.

person GrandmasterB    schedule 30.10.2009
comment
BitBlt не поддерживает прозрачность. Что не имеет большого значения, поскольку Image.GetHBITMAP также не поддерживает его. - person Ian Boyd; 01.11.2009
comment
Затем используйте alphablend (). Дело в том, что прямой GDI намного быстрее, чем использование GDI +. Создайте bmp с помощью GDI +, кешируйте его в потоке (как png, чтобы отслеживать прозрачность) и нарисуйте его с помощью Alphablend вместо gdi +. это будет в 20 раз быстрее. Просто убедитесь, что вы создали его с нужными размерами, которые вам нужны, поскольку AlphaBlend () / BitBlt () не передискретизирует так же хорошо, как функции GDI +. - person GrandmasterB; 09.11.2009
comment
В любом случае, снижение производительности при создании / уничтожении объекта Graphics не минимально. И GDI не поддерживает все операции рисования, которые выполняет GDI +. Кто-то может свободно придумать свою собственную программную библиотеку для рисования, которая внутренне использует GDI, когда это возможно. Назовите это GDI ++. - person Ian Boyd; 23.11.2009