Delphi TCanvas объект поврежден после использования из DLL, как восстановить?

Есть проблемы. У меня есть форма с холстом, мне нужен доступ к этому холсту из DLL по его ручке. Я делаю это таким образом:

из длл

canvas := TCanvas.Create;
  try
    canvas.Handle := handle;
    // do some painting on this canvas
  finally
    canvas.free;
  end;

Работает хорошо, я рисую то, что мне нужно от dll. Но этот трюк имеет побочный эффект. После рисования из dll форма теряет настройки шрифта (кстати, я не использовал шрифты при рисовании из dll, только несколько ректов) и когда я рисую на том же холсте из основной формы, даже если я выполняю напрямую canvas.font.size:= ... canvas.font.name:= ...; до canvas.TextOut, шрифт не меняется. Линии, заливка и другие картины в порядке. Но шрифты становятся поврежденными (иногда нет, но в основном).

Есть ли способ сбросить / переустановить объект TCanvas формы?

3 ответа

Решение

Canvas не имеет функции сброса, но вы можете попросить API сохранить состояние контекста устройства холста и восстановить его после рисования.

var
  SavedDC: Integer;

  ...
  SavedDC := SaveDC(handle);
  try
    canvas := TCanvas.Create;
    try
      canvas.Handle := handle;
      // do some painting on this canvas
    finally
      canvas.free;
    end;
  finally
    RestoreDC(handle, SavedDC);
  end;


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

Причина вашей формы Canvas получает "поврежден", потому что DLL TCanvas объект заменяет оригинал HFONT, HBRUSH и / или HPEN объекты, которые уже были назначены HDC, но затем присваивает фондовые объекты GDI (из GetStockObject()) во время его уничтожения, вместо переназначения исходных объектов GDI, которые были ранее назначены. Это происходит в TCanvas.DeselectHandles() метод, когда TCanvas.Handle Значение свойства изменяется (в том числе при уничтожении):

var
  ...
  StockPen: HPEN;
  StockBrush: HBRUSH;
  StockFont: HFONT;
  ...

procedure TCanvas.DeselectHandles;
begin
  if (FHandle <> 0) and (State - [csPenValid, csBrushValid, csFontValid] <> State) then
  begin
    SelectObject(FHandle, StockPen);   // <-- STOCK PEN!
    SelectObject(FHandle, StockBrush); // <-- STOCK BRUSH!
    SelectObject(FHandle, StockFont);  // <-- STOCK FONT!
    State := State - [csPenValid, csBrushValid, csFontValid];
  end;
end;

...
initialization
  ...
  StockPen := GetStockObject(BLACK_PEN);
  StockBrush := GetStockObject(HOLLOW_BRUSH);
  StockFont := GetStockObject(SYSTEM_FONT);
  ...

Чтобы сделать "сброс" формы Canvas после выхода из функции DLL вам придется обмануть Canvas чтобы знать, что его объекты GDI больше не присваиваются HDC так что он может очистить соответствующие флаги от его внутреннего State член и переназначить его объекты GDI по мере необходимости. Вы также можете:

  1. вручную вызвать OnChange обработчики событий Canvas.Font, Canvas.Brush а также Canvas.Pen свойства:

    procedure TMyForm.FormPaint(Sender: TObject);
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        Canvas.Font.OnChange(nil);
        Canvas.Brush.OnChange(nil);
        Canvas.Pen.OnChange(nil);
      end;
    end;
    

    Или же:

    type
      TGraphicObjectAccess = class(TGraphicObject)
      end;
    
    procedure TMyForm.FormPaint(Sender: TObject);
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        TGraphicObjectAccess(Canvas.Font).Changed;
        TGraphicObjectAccess(Canvas.Brush).Changed;
        TGraphicObjectAccess(Canvas.Pen).Changed;
      end;
    end;
    
  2. Вы можете временно удалить, а затем переназначить оригинал HDC, который имеет аналогичное влияние на State флаги:

    procedure TMyForm.FormPaint(Sender: TObject);
    var
      DC: HDC;
    begin
      try
        CallDllFunc(Canvas.Handle);
      finally
        DC := Canvas.Handle;
        Canvas.Handle := 0;
        Canvas.Handle := DC;
      end;
    end;
    
  3. использование SaveDC() а также RestoreDC(), как показано в ответе Сертака.

Стандартный класс TCanvas не очень подходит для рисования на "заимствованных" полотнах. То есть, беря контекст устройства (например, из какого-либо другого объекта canvas) и используя его в другом, отдельном TCanvas из-за того, как он управляет объектами GDI (полагаясь на "владение" HDC и состояние объектов GDI в этом DC, что оно работает с).

В простых случаях это может работать, но в остальном проблемы, с которыми вы сталкиваетесь, не редкость. В частности, с DLL могут возникнуть проблемы, связанные с тем фактом, что в TCanvas существуют механизмы, основанные на "глобальном" списке полотен (CanvasList), которые необходимо контролировать и синхронизировать в ответ на системные изменения.

то есть в DLL будет CanvasList, который представляет собой список холстов в DLL, отдельный от CanvasList в процессе приложения хоста. Приложение CanvasList не будет включать экземпляры TCanvas в DLL, и наоборот. Если в DLL есть TCanvas, который на самом деле является "дубликатом" TCanvas в приложении (с использованием того же HDC), то должно быть очевидно, как могут возникнуть проблемы.

Я вижу два пути продвижения вперед в вашем случае, которые можно использовать по отдельности или вместе.

  1. Вы не предоставили подробную информацию обо всем своем коде рисования, поэтому трудно сказать, что, вероятно, является источником вашей проблемы. Однако вы можете легко идентифицировать это, закомментировав весь код рисования (между попыткой и, наконец, в вашей программе рисования). Это должно исправить вашу проблему со шрифтом. Если вы затем повторно активируете свой код рисования постепенно (построчно или раздел за разделом), вы можете точно определить, какие операции рисования вызывают проблему, и оттуда (потенциально) определить решение.

  2. Если ваши операции рисования очень просты (просто рисуйте несколько прямоугольников, как вы говорите), то вы можете использовать простые вызовы GDI для рисования в проблемных случаях (или всех них) вместо использования холста. В этом случае я бы предложил передать дескриптор окна в свою DLL, а не в контекст устройства. Ваша DLL должна затем получить свой собственный контекст устройства через GetDC() и выпустить его через ReleaseDC(), когда закончите. Вам нужно будет управлять объектами GDI при рисовании в контексте устройства самостоятельно, но затем вы можете быть уверены, что независимо от того, что вы делаете, вы не вмешиваетесь в объекты GDI, управляемые чертежом TCanvas в том же окне.

Другая возможность заключается в использовании SaveDC() а также RestoreDC(), как показано в ответе Сертака.

Другие вопросы по тегам