Версия для печати


VCL.TBitmap.Ошибки после Dormant
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1092

Денис Зайцев
дата публикации 01-12-2004 12:58

КАТЕГОРИЯБИБЛИОТЕКА.VCL.TBitmap.Ошибки после Dormant
ПРОДУКТDelphi 6,7
ПЛАТФОРМА


В Delphi 6-й и 7-й версии (проверялось в Delphi Enterprise 6.0 build 6.163, Delphi Enterprise 7.0 build 4.453) после вызова метода Dormant объекта TBitmap некорректно работает изменение значений свойств Width, Height, Palette этого объекта (возможно, также некорректно работают некоторые другие методы, не проверялось). Замеченный эффект — очистка изображения при изменении этих свойств, хотя теоретически возможны другие побочные эффекты, например, коррупция памяти.

Пример

  // Загружаем некоторое непустое изображение в объект класса TBitmap
  Bitmap.LoadFromFile(FileName);
  Bitmap.Dormant; // Вызываем Dormant
  Bitmap.Width := Bitmap.Width + 1; // Увеличиваем ширину на 1
  // Ширина увеличилась, но изображение уже пустое!
В предыдущих версиях Delphi (проверялось в Delphi Client/Server 4.0 build 5.103 UP 3, Delphi Enterprise 5.0 build 6.18 UP 1) приведённый кусок кода работает корректно (изображение не очищается).

Нельзя сказать, что какая-то одна ошибка приводит к указанной проблеме. Имеется комплекс причин, связанный со сложностью реализации TBitmap.

Метод Dormant служит для освобождения ресурсов GDI, связанных с изображением, что может быть полезно при работе программы в Windows 9x. Если Ваше приложение держит в памяти много созданных объектов TBitmap, но одновременно использует немногие, Вы можете для неиспользуемых объектов вызывать метод Dormant, тем самым освобождая ограниченные в Windows 9x ресурсы GDI (но сохраняя изображение в памяти!). При любой работе с изображением, которое требует ресурса HBITMAP, этот ресурс автоматически создаётся вновь из изображения, сохранённого в памяти.

Однако в ранних версиях Delphi (во всяком случае, в 5-й) в действительности метод Dormant не освобождал все ресурсы GDI, связанные с изображением. Он был реализован через метод DIBNeeded, который, в свою очередь, вызывал функцию Win32 API CreateDIBSection. Последняя возвращала значение типа HBITMAP, которое помещалось в FImage.FDIBHandle (FImage - это объект класса TBitmapImage, принадлежащий TBitmap) и в дальнейшем возвращалось при чтении свойства TBitmap.Handle. Ресурс HBITMAP, созданный функцией CreateDIBSection, не уничтожался при вызове Dormant.

В 6-й и последующих версиях Delphi метод Dormant реализован иначе. Метод DIBNeeded не вызывается, вместо этого создаётся поток TMemoryStream, в котором сохраняется изображение, все же ресурсы GDI, связанные с объектом TBitmap (DDB и DIB-секции), уничтожаются. Таким образом, казалось бы, класс TBitmap стал работать лучше, чем в предыдущих версиях, в действительности используя меньше ресурсов GDI.

Но из-за этого пришлось изменить метод HandleNeeded, который, как видно из названия, служит для восстановления HBITMAP, требуемого для вызовов функций GDI. Теперь HandleNeeded при отсутствии сохранённого HBITMAP вызывает метод LoadFromStream для восстановления изображения, сохранённого в потоке TMemoryStream. Однако, как это и указано в комментариях программистами Borland, метод LoadFromStream может уничтожить текущий объект FImage (и создать новый). Вот кусок реализации TBitmap.HandleNeeded, который появился в 6-й версии Delphi, но отсутствовал в предыдущих версиях:

  if   (FImage.FHandle = 0)
   and (FImage.FDIBHandle = 0)
   and (FImage.FSaveStream <> nil) then
  begin
    {...}
    LoadFromStream(FImage.FSaveStream);  // Current FImage may be
                                         // destroyed here
    {...}
  end;
В действительности так и происходит, при вызове HandleNeeded (после вызова Dormant) текущий FImage уничтожается и создаётся новый. Это приводит к тому, что реализация некоторых методов класса TBitmap становится некорректной. Это относится к тем методам, в которых стоит:
  with FImage do
  begin
    {...}
    HandleNeeded; // Здесь FImage уничтожается и создаётся вновь,
                  // возможно, по другому адресу
    {...}
    // Здесь любое обращение к полям/методам FImage. Оно некорректно,
    // поскольку работает со старым адресом FImage, сохранённым
    // в начале оператора with
  end;
Так, например, реализованы методы SetWidth, SetHeight, SetMonochrome, SetPalette. Возможно, ошибка проявляется и в других методах. Как видим, здесь происходит обращение к некорректным адресам памяти, что в принципе может вызвать какую угодно ошибку. Возможно даже, что ошибка не проявится вообще, если по счастливому стечению обстоятельств новый объект FImage будет распределён по тому же адресу в памяти, где находился старый.


Типовые решения
  1. Самостоятельно внести изменения в VCL, изменив реализацию методов класса TBitmap, в которых используется конструкция with FImage... таким образом, чтобы после вызова HandleNeeded закрывать оператор with и начинать новый оператор with FImage... Любопытно, что это уже сделано в 7-й версии Delphi (build 4.453) в реализации метода TBitmap.MaskHandleNeeded, и вызывает удивление, что программисты Borland не пошли дальше и не исправили аналогичные ошибки в других методах.
  2. Перед тем, как станет ясно, что с данным TBitmap будет вестись работа, требующая HBITMAP (по крайней мере перед присвоением свойств Width, Height, Palette, Monochrome), вручную читать свойство Handle:
         Dummy := Bitmap.Handle;
    При этом создастся HBITMAP, который будет запомнен в TBitmap. При последующих вызовах метода HandleNeeded объект FImage не будет пересоздаваться и ошибка не возникнет. Когда станет ясно, что работа с TBitmap завершена, можно вновь вызывать Dormant.
  3. Не использовать метод Dormant, попробовав другие средства для экономии ресурсов GDI.

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