Session 5

Adding right-mouse events, drag and drop, and a dialog box to the editor
(WM_DROPFILES, OnMouseDown, Multiple forms)

Session #5

This session, I added some features to the editor that enhance the user interface. The two big ones are 1) using the right mouse button to paint in the empty (background) color and 2) to open a data file by allowing the user to drag and drop from Windows Explorer. I also show how to add another form (an About box in the Help menu, and a configuration form in the Edit menu) to the application.

  1. Painting with the right mouse button. There are really only two small places in the code that needed to be added/modified to support this. The first one is the addition of an OnMouseDown event handler. The second is a slight modification to the OnMouseMove event handler. You can compare last session's code to this one to see the changes. (Use WinMerge or something similar for a good view of the differences.) Look at the online help to see what information is stored in the TMouseButton and TShiftState parameters. As you might suspect, they contain information about which mouse button was pressed and which control keys (alt, ctrl, shift) are being pressed so you can perform different functions with Ctrl+Left-Click, Shift-Right-Click, Ctrl+Shift-Left-Click, etc. Any combination you want to use is available.
    /*
      Handler for the OnMouseDown event. The Shift parameter contains
      additional information such as the state of the control/alt/shift keys.
    */
    void __fastcall TfrmMain::gridWorldMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
    {
        // If the right mouse button is pressed down
      if (Button == mbRight)
      {
          // Convert the x/y pixel location to a row and column in the grid
        TGridCoord coord = gridWorld->MouseCoord(X, Y);
    
          // Update current row/column
        FCurrRow = coord.Y;
        FCurrColumn = coord.X;
    
          // Set this cell as empty (background color)
        FDataMap[FCurrRow][FCurrColumn] = ctEMPTY;
    
          // Paint the cell
        HighlightCell(true);
      }
    }
    
    Technically, adding this feature is trivial, but it enhances the user interface significantly. In fact, as I was "cobbling" this tile editor together and testing it, I kept wishing that I didn't have to keep switching between the Obstacle color and the Empty color. I thought it would have been so much more efficient to just toggle between the two colors by using the two mouse buttons. That is a very intuitive action and many paint programs do something similar. Many times, you'll find that it's the little things like this that distinguish between very good, usable programs, and programs that won't get used. It's all about the interface. And the more the interface stays out of the user's way, the better. Remember: Think about the user's work flow.

  2. Supporting drag and drop from Windows Explorer. This is another user interface enhancement that is easy to implement, yet enhances the program's usability tremendously. Honestly, I can't stand working with any kind of application that doesn't allow dropping files onto it, assuming that it makes sense to do so. (And this comes from a guy that was born and raised on the command line!) Handling files that are dropped onto the main window is straight-forward. After setting up some message-handling code, you simply need to implement the functionality you want to achieve. In this case, when a data file (CS230 collision text file, Exported.txt by default) is dropped onto the window, we want to load it into the editor. The behavior is exactly the same as if the user browsed for the file with the File Open dialog and selected a file from there. Drag and drop is much more natural (eventually!) than navigating the file system. These are the steps to remember:
    1. In the header file, add a declaration for a method that will handle the WM_DROPFILES message from Windows. You can put it in the private section. We will implement this in the .cpp file. Information about the WM_DROPFILES message can be seen here. (Keep in mind that Microsoft is notorious for moving pages on their website, so if the link is broken, you might have to search for it.)
      void __fastcall WMDropFiles(TWMDropFiles &Message);
      
    2. In the header file, add a MESSAGE_MAP to the form's class (private or public). This is boiler-plate macro-magic code for Windows. Some IDEs will actually generate and maintain this message map for you.
      BEGIN_MESSAGE_MAP
      MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WMDropFiles)
      END_MESSAGE_MAP(TForm)
      
    3. In the implementation file, register the application with Windows as being a potential drop-target. Put this in the form's OnCreate method:
        // Register this window with the OS as a drop target.
      DragAcceptFiles(this->Handle, true);
      
    4. (Optionally) In the implementation file, unregister the application with Windows as being a potential drop-target. Put this in the form's OnDestroy method:
        // Unregister this window with the OS as a drop target.
      DragAcceptFiles(this->Handle, false);
      
    5. Now, add the desired functionality by implementing the event handler in the implementation file:
      /*
        Handler for the WM_DROPFILES message from Windows.
      */
      void __fastcall TfrmMain::WMDropFiles(TWMDropFiles &Message)
      {
        int num_files, filename_length;
        char file_name[MAX_PATH + 1];
      
          // Refer to the Win SDK documentation about the details
          // of DragQueryFile and the HDROP structure
      
          // Get the number of files that are being dropped on the window
        num_files = DragQueryFile((HDROP)Message.Drop, -1, NULL, 0);
      
          // Unlikely to happen
        if (num_files < 1)
          return;
      
          // Retrieve each filename that was dropped
        for(int i = 0; i < num_files; i++)
        {
            // Get the length of the i'th filename
          filename_length = DragQueryFile((HDROP)Message.Drop, i, NULL, 0);
      
            // Retrieve the filename from the HDROP structure
          DragQueryFile((HDROP)Message.Drop, i, file_name, filename_length + 1);
      
            // Reset the grid
          Init();
      
            // Read in the data
          ImportMapData(file_name);
      
            // Update the UI
          short r = FRows;
          short c = FColumns;
          spnRows->Position = r;
          spnColumns->Position = c;
      
            // We can only deal with one at this point so bail out.
            // If we could handle multiple files, we would loop through all of them.
          return;
        }
      }
      
  3. Adding a second form to the application. It is pretty easy to add additional forms to the program. In the first example, I simply added a pre-built "About dialog" that will be used to hold information about the program.
    1. Choose "File | New | Other..." from the File menu. Select the item in the left pane called "C++Builder Files". Select "About box" from the list on the right. (It should be the first one.)
    2. Now, add this class' header file to the main form. You can do this "manually" or you can do it by selecting the MainForm.cpp file in the IDE and from the File menu choose "Use Unit...". In the dialog box, choose "ABOUT". This process simply adds the About form's header file to your .cpp file.
    3. Finally, add the code that will display the About box.
      AboutBox->ShowModal();
      
      You can put this code anywhere. The obvious place is to make a Help menu that has an "About..." choice. When the user selects it, the dialog should be displayed. (See the project for details.) Also, if you want the dialog box to be non-modal, call the Show() method instead of ShowModal().

Exercises

  1. Add icons to the Edit and Help menu items. Use any that you see fit. (Google for free icons, or create your own.)
  2. Add shortcut keys to show the Configuration and About forms.
  3. Implement a "cleaner" About dialog. Do this by giving better names to all of the components and by filling in the information in the dialog. This is an easy task, but it will give you good practice in navigating between the visual form and the code behind it.
  4. Make the About dialog resizable. Currently, it can't be resized, which is generally how this type of dialog should work. Keep in mind that you must make sure that the components also size/place themselves properly in the window as it is resized.
  5. Add some error handling for dropping (or opening) "bad" files. Right now, I can drop a .jpg file (or any file) onto the application and it does nothing.