Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

CSSSandbox

4.88/5 (13 votes)
12 Feb 2013CPOL6 min read 27.7K   1.3K  
A tool for simultaneous editing and viewing of CSS and HTML.

CSSSandbox screen shot

Introduction

This article provides a simple side by side CSS/HTML editor and web browser, allowing one to edit the CSS/HTML code on one side and immediately see the results in the browser on the other. In addition to providing a usable tool, it walks through the process of creating that tool by integrating a couple other CodeProject articles and discusses the issues and workarounds involved in getting the pieces to work together.

Background

When developing native/Win32 applications for Windows using C++/MFC, I've come across multiple cases where displaying some information to the user in CSS/HTML is an attractive option as opposed to using a series of regular controls or doing custom DC rendering. Generating CSS/HTML from the C++/MFC application and then presenting it to the user as part of the interface gives you the power and flexibility of CSS/HTML in a Windows application and can result in nice, clean looking output for the end user.

Creating the CSS/HTML to be generated by the application involves trial and error editing of the code and viewing of that code in a web browser. Since the end goal is to render the HTML using the Microsoft Web Browser ActiveX control in an MFC dialog, I thought it might be of use to write a small tool which included that very control along with a simple text editor window. The goal was to be able to do simple experimentation with CSS/HTML and see the results immediately in the Microsoft Web Browser ActiveX control.

I had recently discovered Gary R. Wheeler's excellent article on Code Project, "Using the Web Browser control, simplified", while dealing with the pains of again trying to incorporate the Microsoft Web Browser ActiveX control in a dialog for the purpose already discussed. This process ends up being a bit of a headache, but finding Gary's article was a big help. With that, you can easily drop the browser control into a dialog and push generated HTML strings directly into the control (as opposed to having to go through a temp file and use the file URL). Having discovered this, it seemed like a great opportunity to write a small editing tool, using both Gary's code and the built-in rich edit control. This would allow editing CSS/HTML code and immediately seeing the rendered result side by side.

I knew I wanted a simple dialog app for the tool, but showing an editor and a browser control side by side in such an app demanded both a splitter bar and a resizable dialog to be of use. I dug around for a bit trying to figure out how to use a splitter bar in a dialog and ran across a great article by Lucman Abdulrachman, "Splitter in an MFC dialog based application". The goal was then to integrate the browser control in a dialog, along with the splitter control, and the Windows rich edit control. With those pieces put together, the result should be a tool letting you edit and view the code at the same time.

Integration Issues

The first issue I ran into was that the splitter control was not designed for a resizable dialog. When the dialog size changes, I wrote code which sizes/positions the controls in a way that makes sense. So if you grow the dialog, for example, the controls end up still looking correct and using the available window space. The splitter bar position (as a ratio of position to client width) is maintained through the resize. The problem was that after the resize, since the splitter was apparently not designed for a resizable dialog, the splitter would lock up and no longer move. The code causing the issue appeared to be related to limiting the extremes of the splitter movement. To get this working, I replaced the existing limiting code and added min and max splitter position variables. These are then used in the modified mouse movement handler as shown below.

C++
class CControlSplitter : public CButton
{    
    void SetMinMaxSplitterPos(int minSplitterPos, int maxSplitterPos);
private:
    int minSplitterPos;
    int maxSplitterPos;
};

CControlSplitter::CControlSplitter()
{
    minSplitterPos = -1;
    maxSplitterPos = -1;
}

void CControlSplitter::SetMinMaxSplitterPos(int minSplitterPos, int maxSplitterPos)
{
    this->minSplitterPos = minSplitterPos;
    this->maxSplitterPos = maxSplitterPos;
}

void CControlSplitter::OnMouseMove(UINT nFlags, CPoint point) 
{        
    if(m_bDragging)
    {
        CRect rect;
        ::GetWindowRect(m_hWnd,&rect);        
        CPoint ptMouse;
        GetCursorPos(&ptMouse);
        CSize sizeDiff = ptMouse - m_ptStartDrag;
        CSize sizeMove = m_ptStartPos-rect.TopLeft();
        CRect rectBefore = rect;
        rect.OffsetRect(sizeMove);
        if(m_nType == CS_HORZ)
        {                            
            rect.OffsetRect(0, sizeDiff.cy);
        }else{
            rect.OffsetRect(sizeDiff.cx,0);
        }
        CRect rectAfter = rect;
        CRect cr = rect;
        GetParent()->ScreenToClient(cr);
        if (m_nType == CS_HORZ)
        {
            if ((minSplitterPos >= 0) && (rectAfter.top < rectBefore.top))
            {
                if (cr.top <= minSplitterPos)
                    return;
            }
            else if ((maxSplitterPos >= 0) && (rectAfter.bottom > rectBefore.bottom))
            {
                if (cr.bottom >= maxSplitterPos)
                    return;
            }
        }
        else if (m_nType == CS_VERT)
        {
            if ((minSplitterPos >= 0) && (rectAfter.left < rectBefore.left))
            {
                if (cr.left <= minSplitterPos)
                    return;
            }
            else if ((maxSplitterPos >= 0) && (rectAfter.right > rectBefore.right))
            {
                if (cr.right >= maxSplitterPos)
                    return;
            }
        }
        
        // ....
    }
}

The above code enforces optional min and max limits on the splitter control (in client coordinates). If the splitter is moving left/up and its new position violates the min limit, the movement is stopped and similar logic is used for the max limit. Using this modified code seemed to resolve the issues which were occurring after the resize of the dialog and still prevented the splitter from being moved farther than desired to either side.

Another interesting issue encountered was with the browser control refresh. When the dialog was first opened the browser displayed properly. After dragging the splitter bar, the browser window would be sized appropriately but would no longer render. It was as though the control was not there as all that could be seen was the background. By clicking and dragging around where the window was, you would get text in the browser selected causing it to render. So there appears to be something off with rendering of the browser ActiveX control after its been resized in a dialog. This doesn't appear to be an issue with the code in Gary's article, rather some sort of limitation/glitch with the ActiveX control itself. The workaround I ended up with was using the splitter bar notification and hiding then immediately showing the browser window as shown below. Sort of a hack-around, but it provided the desired result of being able to drag the splitter and still actually see the browser contents!

C++
LRESULT CSSSandboxDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    // called whenever the splitter control is moved by the user
    if (message == UWM_SPLIT_MOVED)
    {
        // need to hide and show the browser window - otherwise it gets into a state where
        // it just doesn't render - not sure why but this workaround seems to help.
        browser.ShowWindow(SW_HIDE);
        browser.ShowWindow(SW_SHOW);

        // get updated splitter vars and save them
        UpdateSplitterVars();
        SaveOptions();
    }

    return CDialogEx::WindowProc(message, wParam, lParam);
}

The message was directly handled in the WindowProc method since its a message registered at runtime by the splitter control code. Using the normal MFC ON_MESSAGE macro didn't work as a result.

With the above issues handled, it was possible to edit code in the rich edit control and see the results immediately in the browser window. Buttons along the bottom of the edit control were provided for saving/loading the file and it soon became apparent that hotkeys would be desirable to allow quickly saving the working file without unnecessary mouse clicks. Also, hitting TAB while editing the code set the focus to the next control in the dialog as opposed to indenting in the code. To get the hotkeys and TAB to work, the below was used.

C++
#define CHECKVKEYSTATE(vkey) \
    ((GetAsyncKeyState(vkey) & (1 << 15)) != 0)

BOOL CSSSandboxDlg::PreTranslateMessage(MSG* pMsg)
{
    //
    // allows using hotkeys (Ctrl+N, Ctrl+O, etc.) to trigger the buttons
    //
    if (pMsg->message == WM_KEYDOWN)
    {
        if (CHECKVKEYSTATE(VK_CONTROL))
        {
            switch (pMsg->wParam)
            {
            case _T('N'):
                OnBnClickedNewButton();
                return TRUE;
            case _T('O'):
                OnBnClickedLoadButton();
                return TRUE;
            case _T('S'):
                OnBnClickedSaveButton();
                return TRUE;
            case _T('R'):
                OnBnClickedRefreshButton();
                return TRUE;
            }
        }
    }

    //
    // allows TAB key to insert a specified # spaces rather
    // than switching focus to the next control in the dialog
    //
    if (GetFocus()->GetSafeHwnd() == editor.GetSafeHwnd())
    {
        if ((pMsg->message == WM_KEYDOWN) && (pMsg->wParam == VK_TAB))
        {
            CString s;
            for (int i = 0; i < NUM_SPACES_ON_TAB; i++)
                s += _T(" ");
            editor.ReplaceSel(s);
            return TRUE;
        }
    }

    return CDialogEx::PreTranslateMessage(pMsg);
}

In the above, the TAB key inserts three spaces into the editor as opposed to inserting an actual TAB character. If you prefer an actual TAB instead or desire a different number of spaces, this can be easily changed in the code.

Using the Code

To use the code, you can download and run it from the demo zip file. Alternatively, you can download the source and build it in Visual Studio 2010. When run for the first time (or if there was no active file when you last exited) the editor will initially load a default document. This default document serves as a readme for the tool, providing basic usage information. This is what is shown in the screenshot in this article.

As you edit code in the editor, you can press Ctrl+R to have the current contents updated in the browser control. You can also check the "Auto Refresh" box which will continually update the browser control as you type. This provides a nice live editing environment where your changes immediately show up in the browser.

Once you've saved the current document, it will automatically be opened the next time you run the application if possible. If you click the New button before exiting, then there will be no active document and it will again open the default file when next run.

Points of Interest

The below CodeProject articles were used in creating this tool.

History

  • 2013.02.11
  • Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)