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.
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!
LRESULT CSSSandboxDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == UWM_SPLIT_MOVED)
{
browser.ShowWindow(SW_HIDE);
browser.ShowWindow(SW_SHOW);
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.
#define CHECKVKEYSTATE(vkey) \
((GetAsyncKeyState(vkey) & (1 << 15)) != 0)
BOOL CSSSandboxDlg::PreTranslateMessage(MSG* pMsg)
{
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;
}
}
}
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.