Tutorial de SDL – Parte 36 – Múltiplas janelas

Continuando nossa série de artigos traduzidos do site lazyfoo.net, veremos agora como manipular múltiplas janelas de uma vez com o SDL.

Um dos novos recursos do SDL 2 é ser capaz de gerenciar múltiplas janelas ao mesmo tempo. Nesse artigo, iremos ver como manipular 3 janelas redimensionáveis ao mesmo tempo.

//Total windows
const int TOTAL_WINDOWS = 3;

class LWindow
{
    public:
        //Intializes internals
        LWindow();

        //Creates window
        bool init();

        //Handles window events
        void handleEvent( SDL_Event& e );

        //Focuses on window
        void focus();

        //Shows windows contents
        void render();

        //Deallocates internals
        void free();

        //Window dimensions
        int getWidth();
        int getHeight();

        //Window focii
        bool hasMouseFocus();
        bool hasKeyboardFocus();
        bool isMinimized();
        bool isShown();

    private:
        //Window data
        SDL_Window* mWindow;
        SDL_Renderer* mRenderer;
        int mWindowID;

        //Window dimensions
        int mWidth;
        int mHeight;

        //Window focus
        bool mMouseFocus;
        bool mKeyboardFocus;
        bool mFullScreen;
        bool mMinimized;
        bool mShown;
};

Aqui temos nosso empacotador da janela que usamos no artigo anterior com alguns ajustes. Queremos ser capazes de capturar o foco e dizer se a janela está sendo exibida, então adicionamos funções para fazer isso.

Cada janela irá ter seu próprio renderizador, assim adicionamos um atributo para guardar essa informação. Também iremos manter um registro do ID da janela para que possamos dizer qual eventos pertencem a qual janela e também adicionamos uma flag para manter um registro de qual janela está sendo exibida.

//Our custom windows
LWindow gWindows[ TOTAL_WINDOWS ];

No exemplo desse artigo, teremos três janelas alocadas globalmente.

bool LWindow::init()
{
    //Create window
    mWindow = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE );
    if( mWindow != NULL )
    {
        mMouseFocus = true;
        mKeyboardFocus = true;
        mWidth = SCREEN_WIDTH;
        mHeight = SCREEN_HEIGHT;

        //Create renderer for window
        mRenderer = SDL_CreateRenderer( mWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC );
        if( mRenderer == NULL )
        {
            printf( "Renderer could not be created! SDL Error: %s\n", SDL_GetError() );
            SDL_DestroyWindow( mWindow );
            mWindow = NULL;
        }
        else
        {
            //Initialize renderer color
            SDL_SetRenderDrawColor( mRenderer, 0xFF, 0xFF, 0xFF, 0xFF );

            //Grab window identifier
            mWindowID = SDL_GetWindowID( mWindow );

            //Flag as opened
            mShown = true;
        }
    }
    else
    {
        printf( "Window could not be created! SDL Error: %s\n", SDL_GetError() );
    }

    return mWindow != NULL && mRenderer != NULL;
}

Aqui temos o código para criação da janela e do renderizado. É basicamente o mesmo código que temos usado desde sempre, só que agora ele é executada de dentro da classe que empacota a janela. Temos que nos certificar de capturar o ID da janela depois de cria-la, pois iremos precisar desse ID para a manipulação dos eventos.

void LWindow::handleEvent( SDL_Event& e )
{
    //If an event was detected for this window
    if( e.type == SDL_WINDOWEVENT && e.window.windowID == mWindowID )
    {
        //Caption update flag
        bool updateCaption = false;
All events from all windows go onto the same event queue, so to know which events belong to which window we check that the event's window ID matches ours.
        switch( e.window.event )
        {
            //Window appeared
            case SDL_WINDOWEVENT_SHOWN:
            mShown = true;
            break;

            //Window disappeared
            case SDL_WINDOWEVENT_HIDDEN:
            mShown = false;
            break;

            //Get new dimensions and repaint
            case SDL_WINDOWEVENT_SIZE_CHANGED:
            mWidth = e.window.data1;
            mHeight = e.window.data2;
            SDL_RenderPresent( mRenderer );
            break;

            //Repaint on expose
            case SDL_WINDOWEVENT_EXPOSED:
            SDL_RenderPresent( mRenderer );
            break;

            //Mouse enter
            case SDL_WINDOWEVENT_ENTER:
            mMouseFocus = true;
            updateCaption = true;
            break;
            
            //Mouse exit
            case SDL_WINDOWEVENT_LEAVE:
            mMouseFocus = false;
            updateCaption = true;
            break;

            //Keyboard focus gained
            case SDL_WINDOWEVENT_FOCUS_GAINED:
            mKeyboardFocus = true;
            updateCaption = true;
            break;
            
            //Keyboard focus lost
            case SDL_WINDOWEVENT_FOCUS_LOST:
            mKeyboardFocus = false;
            updateCaption = true;
            break;

            //Window minimized
            case SDL_WINDOWEVENT_MINIMIZED:
            mMinimized = true;
            break;

            //Window maxized
            case SDL_WINDOWEVENT_MAXIMIZED:
            mMinimized = false;
            break;
            
            //Window restored
            case SDL_WINDOWEVENT_RESTORED:
            mMinimized = false;
            break;

Quando você tem múltiplas janelas, Clicar no botão com o X da janela não significa necessariamente que estamos encarrando o programa. O que faremos ao invés disso é ocultar cada janela quando clicarmos nesse botão. Dessa forma, temos que manter um registro de quando a janela é ocultada/exibida checando pelos eventos SDL_WINDOWEVENT_SHOWN/SDL_WINDOWEVENT_HIDDEN.

            //Hide on close
            case SDL_WINDOWEVENT_CLOSE:
            SDL_HideWindow( mWindow );
            break;
        }

        //Update window caption with new data
        if( updateCaption )
        {
            std::stringstream caption;
            caption << "SDL Tutorial - ID: " << mWindowID << " MouseFocus:" << ( ( mMouseFocus ) ? "On" : "Off" ) << " KeyboardFocus:" << ( ( mKeyboardFocus ) ? "On" : "Off" );
            SDL_SetWindowTitle( mWindow, caption.str().c_str() );
        }
    }
}

Quando você temo múltiplas janelas, clicar no botão com o X é interpretado como um evento SDL_WINDOWEVENT_CLOSE. Quando temos um desses eventos iremos ocultar a janela usando SDL_HideWindow.

void LWindow::focus()
{
    //Restore window if needed
    if( !mShown )
    {
        SDL_ShowWindow( mWindow );
    }

    //Move window forward
    SDL_RaiseWindow( mWindow );
}

Aqui nossa função está capturando o foco de uma janela. Primeiro, checamos se a janela está sendo exibida e em seguida exibimos ela co m SDL_ShowWindow caso não esteja. Depois, chamamos SDL_RaiseWindow para focar nessa janela.

void LWindow::render()
{
    if( !mMinimized )
    {    
        //Clear screen
        SDL_SetRenderDrawColor( mRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
        SDL_RenderClear( mRenderer );

        //Update screen
        SDL_RenderPresent( mRenderer );
    }
}

Como antes, apenas desejamos renderizar se a janela não estiver minimizada.

bool init()
{
    //Initialization flag
    bool success = true;

    //Initialize SDL
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
    {
        printf( "SDL could not initialize! SDL Error: %s\n", SDL_GetError() );
        success = false;
    }
    else
    {
        //Set texture filtering to linear
        if( !SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, "1" ) )
        {
            printf( "Warning: Linear texture filtering not enabled!" );
        }

        //Create window
        if( !gWindows[ 0 ].init() )
        {
            printf( "Window 0 could not be created!\n" );
            success = false;
        }
    }

    return success;
}

Na função de inicialização,  abrimos uma única janela para verificar se a criação da janela está funcionando de forma apropriada.

void close()
{
    //Destroy windows
    for( int i = 0; i < TOTAL_WINDOWS; ++i )
    {
        gWindows[ i ].free();
    }

    //Quit SDL subsystems
    SDL_Quit();
}

Na função de limpeza, fechamos qualquer janela que esteja aberta.

        //Initialize the rest of the windows
        for( int i = 1; i < TOTAL_WINDOWS; ++i )
        {
            gWindows[ i ].init();
        }

        //Main loop flag
        bool quit = false;

        //Event handler
        SDL_Event e;

Antes de entramos no loop principal, abrimos o restante das janelas que temos.

        //While application is running
        while( !quit )
        {
            //Handle events on queue
            while( SDL_PollEvent( &e ) != 0 )
            {
                //User requests quit
                if( e.type == SDL_QUIT )
                {
                    quit = true;
                }

                //Handle window events
                for( int i = 0; i < TOTAL_WINDOWS; ++i )
                {
                    gWindows[ i ].handleEvent( e );
                }

                //Pull up window
                if( e.type == SDL_KEYDOWN )
                {
                    switch( e.key.keysym.sym )
                    {
                        case SDLK_1:
                        gWindows[ 0 ].focus();
                        break;

                        case SDLK_2:
                        gWindows[ 1 ].focus();
                        break;
                            
                        case SDLK_3:
                        gWindows[ 2 ].focus();
                        break;
                    }
                }
            }

No loop principal, depois de termos lidado com os eventos de todas as janelas, lidamos com alguns pressionamentos de teclas especiais. nesse exemplo, quando pressionamos 1, 2  ou 3, damos foco na janela correspondente.

            //Update all windows
            for( int i = 0; i < TOTAL_WINDOWS; ++i )
            {
                gWindows[ i ].render();
            }
                
            //Check all windows
            bool allWindowsClosed = true;
            for( int i = 0; i < TOTAL_WINDOWS; ++i )
            {
                if( gWindows[ i ].isShown() )
                {
                    allWindowsClosed = false;
                    break;
                }
            }

            //Application closed all windows
            if( allWindowsClosed )
            {
                quit = true;
            }
        }

Em seguida, renderizamos todas as janelas e verificamos cada janela para ver se elas estão sendo exibidas. Se todas elas estiverem fechadas, ajustamos a flag de saída para encerrar o programa.

Para encerrar, cabe salientar que esse exemplo de fato não renderizar nenhuma imagem dentro das janelas. Isso envolveria ter que gerenciais os renderizadores e as janelas e fazer com que elas compartilhem recursos. Não há uma maneira certa de fazer isso, e a melhor maneira depende inteiramente do tipo de aplicação que você estiver construindo. Eu recomendo a leitura da documentação do SDL para entender como os renderizados funcionam e então experimentar para descobrir a melhor forma para você gerenciar seus recursos.

Baixe os arquivos de mídia e do código fontes do exemplo desse artigo aqui