• 05/17/00 07:30 PM
    Sign in to follow this  
    Followers 0

    Developing a GUI Using C++ and DirectX Part 1

    Graphics and GPU Programming

    Myopic Rhino
    At first glance, it may seem like I'm reinventing the wheel; Windows already comes with a very complex, very functional GUI. Unfortunately, while the Windows GUI is great for office apps, quite frequently, it's not suited for games. Games tend to want a more precise control over the GUI than Windows can provide (for example, you may want to use alpha-blending to implement partially transparent windows - easy if you've written your own GUI, but next to impossible using the Windows GUI).

    This article series will walk you though how to create a GUI using C++ and DirectX. The series is divided into several parts, each dealing with a specific aspect of GUI programming. They're meant to be read in order, so you've picked the right article to start with.

    I'm making several assumptions about your knowledge. I'm assuming you know the basics of how event-driven programming works (message queues, etc), and I'm assuming you have a strong grasp of PDL (the commenting language - if you don't know what this is, read Code Complete) and C++. I used C++ to implement my GUI system, because I'm a card-carrying member of the C++ fan club, and because the OOP of C++ works great for implementing window and control types. Note the power of OOP in this solution, and ask yourself if you could do the same thing as easily in C.

    Let's start by defining our scope. It's important to realize up front that we're not remaking Windows 95, we're just trying to get a simple GUI up for a game, so we don't have to implement every single control and GUI construct. We only need a few parts for this simple GUI: a mouse pointer, a generic window, and some dialog controls to place within that window. We're also going to need a resource editor, a program that will allow us to design dialogs by graphically dropping controls at various places.


    [size="5"]Start With The Basics - The Rendering Loop

    I'm going to start at the top, by defining a function that will calculate and draw one frame of our GUI system. Let's call this function RenderGUI(). In PDL, RenderGUI does something like this:

    void CApplication::RenderGUI(void)
    {
    // get position and button status of mouse cursor
    // calculate mouse cursor's effects on windows / send messages
    // render all windows
    // render mouse
    // flip to screen
    }
    Pretty straightforward for now. Basically, we grab the new position and status of the mouse cursor, calculate any changes that are caused by the new data, render all our windows, render the mouse cursor, then push the whole thing to the screen.


    [size="5"]The Mouse

    class CMouse {
    public:
    CMouse(); // boring
    ~CMouse(); // boring
    int Init(LPDIRECTINPUT di); // we'll talk about this later
    int Refresh(void); // we'll talk about this later
    int GetButton(int index)
    {
    if (index < 0 || index > NUMMOUSEBUTTONS) return(0);
    return(m_button[index]);
    }
    void clear(void); // sets all vars to zero

    // makes sure p is a valid on-screen point
    void ConstrainPosToScreenSize(CPoint &p);
    CPoint GetAbsPosition(void) { return(m_absposition); }
    CPoint GetRelPosition(void) { return(m_relposition); }
    enum { NUMMOUSEBUTTONS = 3 }; // three button mouse
    private:
    LPDIRECTINPUTDEVICE m_mousedev;
    char m_button[NUMMOUSEBUTTONS]; // state of buttons
    CPoint m_absposition; // actual screen position
    CPoint m_relposition; // relative position
    };

    Pretty straightforward class definition. We've got three data pieces, m_button, m_absposition and m_relposition, abstracted by two functions, GetButton, GetAbsPosition(), and GetRelPosition(). Then we've got Init and Refresh functions, which initialize the mouse and Refresh its button and position information. The m_mousedev is an interface to our mouse device; we get this interface during Init(), and use it in Refresh to communicate with DirectInput.


    [size="5"]Absolute vs. Relative Position, and DirectInput

    Why am I using DirectInput? It's a matter of taste, actually. There are two ways to get mouse data in Windows - from DirectInput (the way I'm about to show), and via a Win32 API function called GetCursorPos(). The primary difference is that DirectInput will give you "relative" mouse information - that is, the cursor's current position relative to its last position - whereas GetCursorPos will give you the absolute screen coordinates. Absolute positioning is great for GUIs; relative positioning is good when the mouse is used without a cursor, i.e., to look around in a FPS game. You can however, calculate relative from absolute, and vice versa.

    I used DirectInput. This decision was made for many reasons, all of which are outside the scope of this article (three words: multiple-mice systems). GetCursorPos() may be a better solution for you - if that's the case, it should be easy to flesh out the mouse class. DirectInput is more tricky (and more interesting), so the rest of this article will be in DirectInput.


    [size="5"]Initializing DirectInput

    Before we go any further with CMouse, let's look at the code to initialize DirectInput. Note that this code doesn't belong in our CMouse::Init() routine; the DirectInput pointer is used by the entire game, not just the mouse, so the code that inits DirectInput should go in your main init function - the same time you init DirectDraw, DirectSound, etc. A DirectInput interface pointer is different than a DirectInput device pointer; you use DirectInput pointers to get DirectInputDevice pointers. Make sure you understand this distinction. Here's the code to initialize the master DirectInput interface pointer:

    LPDIRECTINPUT di = NULL;

    hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &di, NULL);
    if (FAILED(hr)) {
    // error
    handle_error ();
    }

    // Now that we've got a DirectInput interface, let's begin
    // fleshing out our CMouse by implementing CMouse::Init().

    bool CMouse::Init(LPDIRECTINPUT di)
    {
    // Obtain an interface to the system mouse device.
    hr = di->CreateDevice(GUID_SysMouse, (LPDIRECTINPUTDEVICE*)&m_mousedev, NULL);

    if (FAILED(hr)) { /* handle errors! */ }
    // Set the data format to "mouse format".
    hr = m_mousedev->SetDataFormat(&c_dfDIMouse);

    if (FAILED(hr)) { /* handle errors! */ }
    // Set the cooperativity level
    hr = m_mousedev->SetCooperativeLevel(hwnd,
    DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);

    if (FAILED(hr)) { /* handle errors! */ }
    }

    That code does three important things. First, it gets a valid DirectInput mouse device interface, and puts it in di_mouse. Next, it sets the data format and the cooperative level for the device, basically letting windows know that we want to query the device as a mouse, and that we don't want to take exclusive ownership of it. (Exclusive ownership means that we're the only app that can use the mouse - by specifying DISCL_NONEXCLUSIVE, we've told Windows that we're going to be sharing the mouse with other applications.)


    [size="5"]Polling DirectInput for Mouse Status

    Now let's flesh out CMouse::Refresh(), the function responsible for updating the CMouse's internal button state and position. Here's the code:

    void CMouse::Refresh(void)
    {
    char done = 0;
    int q;
    HRESULT hr;
    CPoint p;
    DIMOUSESTATE dims;

    // clear our struct - eventually, directinput will fill this in
    memset(&dims, 0, sizeof(DIMOUSESTATE));
    if (!m_mousedev) return; // we don't have a pointer! Bail!

    while (!done)
    {
    // query DirectInput for newest mouse data
    hr = m_mousedev->GetDeviceState(sizeof(DIMOUSESTATE), &dims);
    if (FAILED(hr))
    {
    if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
    {
    // device lost... reacquire
    hr = m_mousedev->Acquire();
    if (FAILED(hr))
    {
    // error handling goes here
    clear();
    done=1;
    }
    }
    else
    {
    // it's some other error - handle it
    clear();
    done = 1;
    }
    }
    else // read mouse successfully!
    {
    done = 1;
    }
    } // end while loop - we've read DI correctly

    // squirrel away newest rel position data
    m_relposition.x = dims.lX;
    m_relposition.y = dims.lY;
    m_relposition.z = dims.lZ;
    // now calc abs position from new relative data
    m_absposition.z += dims.lZ;
    m_absposition.x += dims.lX;
    m_absposition.y += dims.lY;

    // keep the mouse pointer on-screen...
    ConstrainPosToScreenSize(m_absposition);
    // get button data
    for (q=0; q < NUMMOUSEBUTTONS; q++)
    {
    m_button[q] = (dims.rgbButtons[q] & 0x80));
    }
    }

    That code's doing a lot of things. First, it queries DirectInput for the new absolute mouse position (there's a while loop in there that will automatically retry the query if we've lost the interface). Next, it squirrels the absolute position data away in m_absposition, then it "applies" the relative position to come up with the new absolute position. The ConstrainPosToScreenSize() makes sure the point is within the bounds of the screen. Finally, it loops through and refreshes all the buttons.


    [size="5"]Drawing The Mouse

    There's two main philosophies behind drawing the mouse cursor. If you know that your entire screen will be refreshed with new pixel data every frame, you can simply blt the mouse cursor at its new absolute position, and be done with it. A better, solution, however, is to grab a copy of the pixel data under the mouse cursor before you blt it, then, when the mouse moves, you erase your old blt by blting your saved pixel data back. I prefer the second method.

    I'm not going to go into the gritty details of blitting surfaces and all that; you should know how to do that.


    [size="5"]Threads and Tails

    If you don't mind multithreading, there's a better way to deal with the mouse than what's described here. The method here is a one thread method, which polls the mouse every frame. Good for high frame rates, but not so good for low frame rates - the mouse cursor will appear "sluggish." The best way to deal with the mouse is to start a separate "mouse-rendering" thread, which continually monitors WM_MOUSEMOVE messages, and takes care of updating and bltting the mouse cursor each time the mouse is moved. The advantage to multithreading the mouse pointer in this manner is that your mouse will still be fluid regardless of how slow your game's frame rate is. Having a separate thread for your mouse will make the game feel responsive, regardless of frame rate.

    Also, it should be obvious to you by now how to create mouse trails. Keep the last several (five or ten) mouse cursor positions in an array. Whenever the mouse moves, discard the oldest coordinate, move all the other coordinates down one slot, and put the newest coordinate in the top slot. Then, if you want to get extra fancy, use alpha-blitting to render the old coordinates with more transparency than the newer ones.


    [size="5"]Stay Tuned..

    Whew! Now we've got the mouse cursor down. Up next, Part II to learn how to create some basic windows, and move them around. Stay tuned for additional excitement!
    0


    Sign in to follow this  
    Followers 0


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!


    Register a new account

    Sign in

    Already have an account? Sign in here.


    Sign In Now

    There are no reviews to display.