Monday, 5 May 2014

Writing a StrPix-like program with Lazarus – 9

IceBerg has done the hard work of converting the raw RGB image data of the *.wad file into the *.bmp format so all we need to do to display the image data is assign the *.bmp to a TImage control.

Go to Window>Form1 to display our form.
Click on the TImage control in the Additional tab of the component palette and then click on the form anywhere to the right of the TreeView1 and Splitter1 controls.

TImage

With the Image1 control selected, click on the Align property in the Object Inspector window and select alLeft from the dropdown list.

ImageAlign

Next click on the AutoSize property in the Object Inspector window and select True from the dropdown list.

ImageAutoSize

Our Image1 control will now automatically adjust its size to the size of the picture we assign to it.
If you build and run the program you will not see any image displayed because we have not assigned any picture to the Image1 control.

In the Object Inspector window click on FileOpen1 (you may need to expand the ActionList1 node first) and then click on the Events tab.

Click OnAccept and then the 3 dot button to jump to the FileOpen1Accept method in the Source Editor window.

Add the following line to the method somewhere after the MakeWadTree method is called.

Image1.Picture.Bitmap := wad.textureMap;

The method should look like this.

procedure TForm1.FileOpen1Accept(Sender: TObject);
var
  err: string;
  valid: integer;
begin
  // Processing all windows messages before and after using the
  // dialog box keeps the user interface clean.
  Application.ProcessMessages;
  ////////////// if not Main.OpenDialogWad.Execute then
  //////////////  exit;
  Application.ProcessMessages;

  // Make sure that wads will not accumulate in memory.
  // Free is successful even if the memory stream was not yet created.
  streamWAD.Free;
  streamWAD := TMemoryStream.Create;
  streamWAD.LoadFromFile(FileOpen1.Dialog.FileName);
  ////////////// Main.TextBox.Caption := Main.OpenDialogWad.FileName;

  // Crawl through the file for validity and find the main sections.
  valid := WADCrawler.IsValidWad(streamWAD, err);
  if valid < 0 then
  begin
    MessageDlg('File is not a valid WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
    exit;
  end
  else if valid > 0 then
    MessageDlg('File is not a regular WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);

  // The main sections were already detected by the crawler.
  // Now parse the WAD file and extract the data tables.
  WADParser.ExtractAll(streamWAD);

  // Build the Wad Tree.
  MakeWadTree;
  //////////////Main.TreeViewWad.FullExpand;

  // Prepare the user data interface.
  //////////////Main.PrepareWadDataForDisplay;
  Image1.Picture.Bitmap := wad.textureMap; //NEW
  // Enable the other file options in the popup.
  //Main.SaveWad.Enabled   := TRUE;
  //Main.SaveAsWad.Enabled := TRUE;
end;

Build and run the program and open a *.wad file and the texture image should now display.

TextureImage

The wad.textureMap bitmap height is taller than our form and so the image is not fully visible. We could place the Image1 control in a TScrollBox so we could then scroll the image the same as is done in WadExplorer but since we want to add more controls beneath the image, we will do what TurboPascal does in StrPix.

In StrPix the TImage control has a fixed width and height of 256 pixels and can display a 256 x 256 segment of the texture map also known as a “page” or “tile”.

So instead of assigning the whole wad.textureMap bitmap to our image we will only copy a 256 x 256 section of it to the image.

We will place the code for displaying the texture map page in a method of its own to keep the FileOpen1Accept procedure tidy and also because we may need to call the code from other procedures.

Go to the interface section of Unit1 and add the following line inside the TForm1 class declaration.

TForm1 = class(TForm)
    ActionList1: TActionList;
    FileExit1: TFileExit;
    FileOpen1: TFileOpen;
    Image1: TImage;
    MainMenu1: TMainMenu;
    MenuItem1: TMenuItem;
    MenuItem10: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem3: TMenuItem;
    MenuItem4: TMenuItem;
    MenuItem5: TMenuItem;
    MenuItem6: TMenuItem;
    MenuItem7: TMenuItem;
    MenuItem8: TMenuItem;
    MenuItem9: TMenuItem;
    Splitter1: TSplitter;
    StatusBar1: TStatusBar;
    TreeView1: TTreeView;
    procedure FileOpen1Accept(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    { private declarations }
    procedure MakeWadTree;
    procedure DisplayTexturePage(pagenumber: integer); //NEW
  public
    { public declarations }
  end; 

Click on DisplayTexturePage to position the cursor there and then click on Source>Code Completion to create the empty method in Unit1.

Add code to the method as shown below.

procedure TForm1.DisplayTexturePage(pagenumber: integer);
var
  sourceR, destR: TRect;
begin
  if wad.textureMap.Empty then Exit;
  if pagenumber < 0 then Exit;
  sourceR := Bounds(0, pagenumber*256, 256, 256);
  destR := Bounds(0, 0, 256, 256);
  // Bitmap must already exist for CopyRect but
  // first time through this procedure it doesn't
  // so we create it with SetSize method
  Image1.Picture.Bitmap.SetSize(256, 256);
  Image1.Picture.Bitmap.Canvas.CopyRect(destR, wad.textureMap.Canvas, sourceR);
end; 

Click Window>Form1 to display our form.

Click on the Image1 control to select it and then in the Object Inspector click on the properties tab and then click Align. From the dropdown list click on alNone.

Click on AutoSize and click on False in the dropdown.

Right click on the Image1 control on the form and click on Size and make the width and height 256.

Resize

Find the FileOpen1Accept method in Unit1 and replace the line

Image1.Picture.Bitmap := wad.textureMap

with

DisplayTexturePage(0);

The method should look like this.

procedure TForm1.FileOpen1Accept(Sender: TObject);
var
  err: string;
  valid: integer;
begin
  // Processing all windows messages before and after using the
  // dialog box keeps the user interface clean.
  Application.ProcessMessages;
  ////////////// if not Main.OpenDialogWad.Execute then
  //////////////  exit;
  Application.ProcessMessages;

  // Make sure that wads will not accumulate in memory.
  // Free is successful even if the memory stream was not yet created.
  streamWAD.Free;
  streamWAD := TMemoryStream.Create;
  streamWAD.LoadFromFile(FileOpen1.Dialog.FileName);
  ////////////// Main.TextBox.Caption := Main.OpenDialogWad.FileName;

  // Crawl through the file for validity and find the main sections.
  valid := WADCrawler.IsValidWad(streamWAD, err);
  if valid < 0 then
  begin
    MessageDlg('File is not a valid WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
    exit;
  end
  else if valid > 0 then
    MessageDlg('File is not a regular WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);

  // The main sections were already detected by the crawler.
  // Now parse the WAD file and extract the data tables.
  WADParser.ExtractAll(streamWAD);

  // Build the Wad Tree.
  MakeWadTree;
  //////////////Main.TreeViewWad.FullExpand;

  // Prepare the user data interface.
  //////////////Main.PrepareWadDataForDisplay;
  DisplayTexturePage(0); //NEW
  // Enable the other file options in the popup.
  //Main.SaveWad.Enabled   := TRUE;
  //Main.SaveAsWad.Enabled := TRUE;
end;  

Build and run the program and the first page of the texture map should be displayed for every *.wad file opened.

We will now add the ability to display any page of the texture image.

Click on Window>Form1 to display the form.

Click on the TComboBox control in the Standard tab of the component palette and then click on the form somewhere below the Image1 control.

ComboBox

With the ComboBox1 control still selected, hold the Shift key and click on the Image1 control.

Now both controls are selected right click on the Image1 control and click on Align.

For the horizontal alignment choose Centers and click OK.

Centre

The ComboBox1 control will be used to select the texture page to be displayed so we need add a line with the page number to ComboBox1 for every page in the texture map.

Since every *.wad file may have a different number of pages we need to do this in code when a *.wad file is opened.

Add the following method to the TForm1 class declaration in the interface section of Unit1 and then create the empty method in the implementation section using Source>Code Completion.

TForm1 = class(TForm)
    ActionList1: TActionList;
    ComboBox1: TComboBox;
    FileExit1: TFileExit;
    FileOpen1: TFileOpen;
    Image1: TImage;
    MainMenu1: TMainMenu;
    MenuItem1: TMenuItem;
    MenuItem10: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem3: TMenuItem;
    MenuItem4: TMenuItem;
    MenuItem5: TMenuItem;
    MenuItem6: TMenuItem;
    MenuItem7: TMenuItem;
    MenuItem8: TMenuItem;
    MenuItem9: TMenuItem;
    Splitter1: TSplitter;
    StatusBar1: TStatusBar;
    TreeView1: TTreeView;
    procedure FileOpen1Accept(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    { private declarations }
    procedure MakeWadTree;
    procedure DisplayTexturePage(pagenumber: integer);
    function CalcNumPages(var bmp: TBitmap): integer; //NEW
  public
    { public declarations }
  end;

You need to add the Graphics unit to the uses clause of the interface if it is not already there because the type TBitmap is declared in the Graphics unit and we have used that type in our interface section.

interface

uses
  Classes, SysUtils, Forms, Dialogs, Menus,
  ComCtrls, ActnList, StdActns, ExtCtrls, StdCtrls, Graphics;

If your uses clause doesn’t match mine it is because I have removed unused units by right clicking on the code in Unit1 and clicking Refactoring>Unused Units.

Units included in the interface uses clause are visible in the implementation section so do not need to be added to the uses clause in the implementation section but the reverse is not the case.

This is the first function we have created. Functions must return a result and one way to do this is to use the variable named “Result” that is automatically declared for functions to hold a function’s result. We do not declare this variable but must make sure that it is assigned a value before the function exits by reaching the function’s “end;” or an “Exit”.

Our function will return the number of pages in a texture map.

Add the following code to the CalcNumPages method.

function TForm1.CalcNumPages(var bmp: TBitmap): integer;
begin
  Result := 0;
  if bmp.Empty then Exit;
  Result := bmp.Height div 256;
end;

We will store the number of texture pages in a global variable so declare the following variable in the implementation section.

implementation

uses TombRaiderWAD, strutils;

{$R *.lfm}

var
  streamWAD: TMemoryStream;
  WADCrawler: TWADCrawler;
  WADParser: TWADParser;
  moveableNames, staticNames: TStringList;
  pagecount: integer; //NEW  

Find Unit1’s FileOpen1Accept method and add the following code. It would probably be better to write this code in a separate procedure for neatness.

procedure TForm1.FileOpen1Accept(Sender: TObject);
var
  err: string;
  valid: integer;
  i: integer; //NEW
begin
  // Processing all windows messages before and after using the
  // dialog box keeps the user interface clean.
  Application.ProcessMessages;
  ////////////// if not Main.OpenDialogWad.Execute then
  //////////////  exit;
  Application.ProcessMessages;

  // Make sure that wads will not accumulate in memory.
  // Free is successful even if the memory stream was not yet created.
  streamWAD.Free;
  streamWAD := TMemoryStream.Create;
  streamWAD.LoadFromFile(FileOpen1.Dialog.FileName);
  ////////////// Main.TextBox.Caption := Main.OpenDialogWad.FileName;

  // Crawl through the file for validity and find the main sections.
  valid := WADCrawler.IsValidWad(streamWAD, err);
  if valid < 0 then
  begin
    MessageDlg('File is not a valid WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
    exit;
  end
  else if valid > 0 then
    MessageDlg('File is not a regular WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);

  // The main sections were already detected by the crawler.
  // Now parse the WAD file and extract the data tables.
  WADParser.ExtractAll(streamWAD);

  // Build the Wad Tree.
  MakeWadTree;
  //////////////Main.TreeViewWad.FullExpand;

  // Prepare the user data interface.
  //////////////Main.PrepareWadDataForDisplay;

  //////////////// start of NEW code
  pagecount := CalcNumPages(wad.textureMap);
  ComboBox1.Clear; // deletes all current entries
  for i := 1 to pagecount do
  begin
    ComboBox1.Items.Append(Format('Page #%d',[i]));
  end;
  if ComboBox1.Items.Count > 0 then ComboBox1.ItemIndex:=0; 
  //////////////// end of NEW code

  DisplayTexturePage(0);
  // Enable the other file options in the popup.
  //Main.SaveWad.Enabled   := TRUE;
  //Main.SaveAsWad.Enabled := TRUE;
end;

If you build and run the program and open a *.wad file you should see that ComboBox1 has an entry for each page of the texture map but selecting a different page doesn’t change the image.

ComboBoxPages

To change the image when a page is selected in ComboBox1 we need to add code to an event handler for ComboBox1.

Select ComboBox1 on the form by clicking it and then click the Events tab in the Object Inspector window.

Click OnChange and then click the 3 dot button that appears.

This will create an empty method in Unit1 and jump to it in the Source Editor.

The code we add to this method will be executed whenever the selected entry in ComboBox1 is changed.

Add the following code.

procedure TForm1.ComboBox1Change(Sender: TObject);
var
  pagenumber: integer;
begin
  if ComboBox1.Items.Count = 0 then Exit;
  pagenumber := ComboBox1.ItemIndex;
  DisplayTexturePage(pagenumber);
end;

With ComboBox1 still selected go to the Properties tab in the Object Inspector and scroll down to Text. Click on Text and delete the text in the edit box next to it. Now ComboBox1 doesn’t appear in ComboBox1 when the program starts.

Text

Since we call the method DisplayTexturePage in the OnChange event handler we can delete the method call in FileOpen1Accept and instead call the ComboBox1Change method.

The ComboBox1Change method requires an argument (an entry in brackets after the method’s name) that has the parameter name “Sender” in the declaration.

We can use “nil” in this case so the FileOpen1Accept method becomes…

procedure TForm1.FileOpen1Accept(Sender: TObject);
var
  err: string;
  valid: integer;
  i: integer;
begin
  // Processing all windows messages before and after using the
  // dialog box keeps the user interface clean.
  Application.ProcessMessages;
  ////////////// if not Main.OpenDialogWad.Execute then
  //////////////  exit;
  Application.ProcessMessages;

  // Make sure that wads will not accumulate in memory.
  // Free is successful even if the memory stream was not yet created.
  streamWAD.Free;
  streamWAD := TMemoryStream.Create;
  streamWAD.LoadFromFile(FileOpen1.Dialog.FileName);
  ////////////// Main.TextBox.Caption := Main.OpenDialogWad.FileName;

  // Crawl through the file for validity and find the main sections.
  valid := WADCrawler.IsValidWad(streamWAD, err);
  if valid < 0 then
  begin
    MessageDlg('File is not a valid WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);
    exit;
  end
  else if valid > 0 then
    MessageDlg('File is not a regular WAD.' + #10 + err,
      mtWarning,
      [mbOK],
      0);

  // The main sections were already detected by the crawler.
  // Now parse the WAD file and extract the data tables.
  WADParser.ExtractAll(streamWAD);

  // Build the Wad Tree.
  MakeWadTree;
  //////////////Main.TreeViewWad.FullExpand;

  // Prepare the user data interface.
  //////////////Main.PrepareWadDataForDisplay;
  pagecount := CalcNumPages(wad.textureMap);
  ComboBox1.Clear;
  for i := 1 to pagecount do
  begin
    ComboBox1.Items.Append(Format('Page #%d',[i]));
  end;
  if ComboBox1.Items.Count > 0 then ComboBox1.ItemIndex:=0;
  ComboBox1Change(nil); //NEW

  // Enable the other file options in the popup.
  //Main.SaveWad.Enabled   := TRUE;
  //Main.SaveAsWad.Enabled := TRUE;
end; 

Now if you build and run the program and open a *.wad file, selecting a different page changes the image accordingly.

You will notice that if you use the mouse wheel over the image the image doesn’t “scroll”.

Thanks to the members of the Lazarus forums, next post we will add code to enable this feature.



prev | next



No comments:

Post a Comment