Part 2 of 5 - Introduction
Links to Other Parts of the Series
The following text is identical to Part 1. If you haven't already read that article, this article will be useless to you, so by all means, catch up. We'll wait here. If you have read the Part 1 article, you can skip these intro sections.
This article series is another in my series of "code we really use" articles. There is no unnecessary discussion about theory, no expounding on technique, and no chest-thumping because I thought it all up myself. It's just a bunch of stuff I did to stand one of our applications up. MOST of the stuff in this article is based on other code that I got from CodeProject, and what follows describes the basis for a project I am actively developing and how I integrated articles and help I got from CodeProject.
Rant
I've been a member on CodeProject for over six years (as of this writing), and I've come to discover some disturbing trends regarding articles. First, article authors tend to post an article and as time goes by, the author essentially abandons the article and people posting questions are greeted with either silence from the author, or a response that says something like "I don't code in this/that language any more". Let's face it, you can't blame them. Many of the articles I use are three or four years old, and I understand that programmers need to move on and that often means completely abandoning older code.
On the other side of the fence are the people that download the source code and samples associated with a given article. Many times, someone will post a question in an article that has absolutely nothing to do with the article itself, but the subject will be related to a certain aspect of the article. As an example, I posted an article about dynamically building a menu. Recently, someone posted a message in that article that asked about adding winhelp to their dynamically built menu. Then there's the people that encounter an issue (real or imagined) with an article, and expect someone else to fix it for them. These people really annoy me. After all, we're all supposed to be programmers here.
So, What's the Point of This Article?
The entire point of this article is to illustrate real-world use of code-snippets, classes and techniques I gleaned from CodeProject over the last six years, including work-arounds to fit code into my sometimes bizarre requirements. Many times, I'll use the VC++ forum to ask a question that will help me understand an article, or massage the article's code for my own use.
Assumptions
The original version of this article started out as a kind of detailed tutorial describing how to use the IDE, and other inane items like that. After a while, I realized this created a huge amount of overhead as far as the article's weight was concerned. Beyond that, I was starting to become bored with the whole thing and I could plainly see that the quality of my writing was beginning to suffer as a result.
The only solution was to start over and make the assumption that you, the user, have a working knowledge of the VS2005 IDE, especially as it relates to creating VC++/MFC applications. This way, we can talk more about the important stuff than suffer through stuff you should already know. I also assume that you have a decent working knowledge of MFC. I'm not saying you have to be an expert, but I assume you can move around in a MFC project without bumping your head on the intricacies of CMainFrame
.
Other Stuff
Sprinkled throughout the article, you'll find "Coding Notes". These simply describe the way I do things when coding, and why I do them. They are certainly not requirements by any stretch of the imagination, but they often concern code readability and maintainability. I'm sure that many of you have your own ways of doing things, but please keep comments regarding these issues to a minimum. After all, this article is not about style.
The total process of coding the complete demo application requires just an hour or so (if you know all the steps ahead of time). Writing this article series has taken me DAYS, so don't be put off by it's length.
The html and images for this article is included in the project download, but doesn't include the pretty CodeProject formatting. If you can mentally handle that, you can simply refer to this .HTML file and get on with your programming.
Finally, I know there are folks out there that vote my stuff a 1 simply because it's, well, something I wrote. I request that you be mature and professional and restrict your politics to the soapbox when voting. Remember, you're voting on the article, not on the author.
What We've Already Done
In part one of this article series, we went through the steps of creating a MFC SDI application and making the view a little more interesting by adding the MFC Grid Control to it. In Part 2, we'll create a flat splitter window that can switch views in the primary pane.
Adding a Splitter Window with Swappable Views
Adding a splitter window is really fairly simple in a MFC application, with all of the work is done in the CMainFrame class. Of course, there's a MSDN article available that describes the process of adding a splitter window, but I hate chasing links to find out how to do stuff, and I bet you probably do, too. So, in the interest of just getting the job done, we'll skip the basic splitter, and go right to the one we really want - the flat splitter window.
In the case of my real-world application, I only needed a horizontal splitter, so this discussion is limited to that requirement. Further, I started with Marc Richarme's Flat Splitter Window article, and then added most of the code from Dan Clark's Multi-View Splitter Window article to get the swappable view functionality. This is a perfect example of using two separate articles on CodeProject to create a single specialized class. Because of the combining of code from these two articles, you should probably use the code I provided in my sample project unless you just want to go through the experience of doing the same thing on your own.
In Part 1, we added the MFC Grid Control to the CSDIMultSpliView
class (created by the Application Wizard). While this is all fine and dandy, we're about to get a bit fancier with swappable views. Toward this end, we need to create the views that we'll be swapping.
Create New View Class - CPrimaryView
This view will be used to display the grid.
- Right-click the SDIMultiApp1 project item in the Solution Explorer pane, in the context menu select Add | Class...
- In the subsequent Add Class dialog, select MFC in the Categories tree (left side of dialog box) and then MFC Class in the Templates list (right side of dialog). Click ADD.
- In the next dialog box, specify a class name (this sample uses
CPrimaryView
, and select the base class. For our sample, we'll use CView
. Click FINISH.
- Move all of the code related to the grid control from the
CSDIMultiSplitView
class to the CPrimaryView
class. You should use the IDE to add the appropriate overrides in this class. If you need detailed instructions, refer to Part 1 of this article series. In short, you need to override OnInitialUpdate
and OnCmdMsg
, and add message handlers for OnSize
and OnEraseBkgnd
. Just copy the code from within the resulting functions from CSDIMultiApp1View
to this class.
- Optional step - remove the grid control code from the CSDIMultiSplit class. We don't need it there because that class is going to be reduced to be just a place holder view for the swappable views. In the sample application provided with this article, I just
ifdef
'd around the grid control code so that it wouldn't be included when the application was compiled.
Create New View Class - CSecondaryView
This will be a simple GDI view that contains text in the form of a report (actually, it will be a simple collection of lines created and displayed in a for
loop). This view will be able to print as well (no easy task as you will soon see).
Since you just did it for the class above, I won't detail the act of creating a new class in this step. When you get the class wizard dialog box, the class name should be CSecondaryView, and it should be derived from CScrollView, as shown below:
Here's a new version of the OnDraw()
function to make the view interesting. Don't be alarmed at the number of lines we're putting on the screen because we'll be using this to test the printing functionality we'll be adding a little later.
void CSecondaryView::OnDraw(CDC* pDC)
{
CDocument* pDoc = GetDocument();
CFont docFont;
CFont* pOldFont;
BuildFont(pDC, &docFont, 11, false, false);
pOldFont = pDC->SelectObject(&docFont);
CString sText = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int nLineHeight = pDC->GetTextExtent(sText).cy + 2;
int nMargin = 10;
int nMaxLines = 70;
for (int i = 1; i <= nMaxLines; i++)
{
sText.Format("This is line number %02d of %d", i, nMaxLines);
}
pDC->SelectObject(pOldFont);
}
And to support the font we need, here's the BuildFont
function that you have to add to the class. The more-eagle-eyed user may recognize this function as being the same one we used in the from the CFlatSplitterWnd
class.
BOOL CSecondaryView::BuildFont(CDC* pDC, CFont* pFont, int nFontHeight,
bool bBold, bool bItalic)
{
nFontHeight = -MulDiv(nFontHeight, pDC->GetDeviceCaps(LOGPIXELSY), 72);
CString sFontName = _T("Arial");
int nWeight = (bBold) ? FW_BOLD : FW_NORMAL;
BYTE nItalic = (bItalic) ? 1 : 0;
return pFont->CreateFont(nFontHeight, 0, 0, 0, nWeight, nItalic, 0, 0,
DEFAULT_CHARSET,
OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, sFontName);
}
Create New View Class - CInfoView
Following the same steps described above, create another new view class called CInfoView
, and once again, set the base class to CSrollView
.
Here's some code to make the view more interesting looking. The only difference between this code and the code we used in the CSecondaryView
class is the number of lines we're putting on the screen. Since we won't be making this view printable, we don't need as many lines with which to test the view.
void CInfoView::OnDraw(CDC* pDC)
{
CDocument* pDoc = GetDocument();
CFont docFont;
CFont* pOldFont;
BuildFont(pDC, &docFont, 11, false, false);
pOldFont = pDC->SelectObject(&docFont);
CString sText = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int nLineHeight = pDC->GetTextExtent(sText).cy + 2;
int nMargin = 10;
int nMaxLines = 20;
for (int i = 1; i <= nMaxLines; i++)
{
sText.Format("This is line number %02d of %d", i, nMaxLines);
}
pDC->SelectObject(pOldFont);
}
Finally, put a copy of the BuildFont()
function into this class as well.
Go ahead and check the Solution Explorer pane for the appropriate files (or the ClassView pane if that's what trips your trigger). All that's left to complete this view's basic functionality is to add some code in OnDraw
that fills the view up with text. This will server two purposes - providing immediate and gratuitous visual feedback when we switch between this view and the grid view, and setting us up to add some printing functionality a little later in the article. For now, here's a new OnDraw
function:
Create New View Class - CSecondaryView
Following the same steps described above, create another new view class called CSecondaryView.
Create New View Class - CInfoView
Following the same steps described above, create another new view class called CInfoView.
Here's some code to make the view more intersting looking.
Implement The Splitter Window with Swappable View Support
One of the tasks for our real-world application was to provide a split view that allows the user to see some specific type of data in another window. The only real requirements provided were that the splitter bar be visible all the time, and that it split the main window horizontally. In the interest of brevity and keeping everything within the intended scope, we'll ignore other kinds of splits and issues regarding the combination of horizontal and vertical splitters.
Beyond the stated requirements, it was left to me to make it happen anyway I wanted. After looking around a bit on CodeProject, I found a good starting point with the FlatSplitterWnd class from Marc Richarme. Essentially, this class provided me with an always-visible flat splitter window, and was my job to make it pretty. I chose to make the splitter bar itself yellow, and put the word "Info" on the bar with two arrowheads pointing down. The idea here was to make it easy to see. Without further delay, here are the steps I followed to implement this feature.
- So, first we're going to add the CFlatSplitterWnd files to our project. Once again, I prefer to place files from external sources into their own folders (yes, even if I modify them like we're going to do in this case), so after you've downloaded the FlatSplitterWnd article's source code, extract the FlatSplitterWnd.CPP and FlatSplitterWnd.H files to the folder of your choice (I put them into \SDIMultiSplit\FlatSplit). Since we've performed similar steps when we added the MFC Grid Ctrl, I don't think there's any reason to illustrate how to add files to a project a second time. Just Right click on the SDIMultiSplit project in the Solution Explorer, click Add | Existing... in the context menu, and the browse to the folder in which you placed the FlatSplitterWnd files, and click ADD.
- If you put these files into their own folder like I did, you have to add the folder to your project settings Addition include directories. Make sure you do this for all configurations.
- Now that we have our files where we want them, let's look at the changes I made to implement my visual styling on the splitter bar. Open FlatSplitterWnd.CPP, and look at the constructor for the class.
CFlatSplitterWnd::CFlatSplitterWnd()
{
m_cxSplitter = m_cySplitter = 3 + 1 + 1;
m_cxBorderShare = m_cyBorderShare = 0;
m_cxSplitterGap = m_cySplitterGap = 3 + 1 + 1;
m_cxBorder = m_cyBorder = 1;
}
Since we need room for our bar text, we need to make the bar taller, so I changed the code above to this:
CFlatSplitterWnd::CFlatSplitterWnd()
{
m_cxSplitter = m_cySplitter = 15 + 1 + 1;
m_cxBorderShare = m_cyBorderShare = 0;
m_cxSplitterGap = m_cySplitterGap = 15 + 1 + 1;
m_cxBorder = m_cyBorder = 1;
m_bFirstView = true;
}
- Next, we need to modify the OnDrawSplitter function. This is where all of the excitement happens regarding the color/style of the splitter bar. Here's the original function.
void CFlatSplitterWnd::OnDrawSplitter(CDC* pDC, ESplitType nType,
const CRect& rectArg)
{
if((nType != splitBorder) || (pDC == NULL))
{
CSplitterWnd::OnDrawSplitter(pDC, nType, rectArg);
return;
}
ASSERT_VALID(pDC);
pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW),
GetSysColor(COLOR_BTNHIGHLIGHT));
}
The key to my modification lies in the ESplitType parameter. There are four possible values we can expect, but we're only interested in one - splitBar. The revised version of the OnDrawSplitter function (shown below) includes sufficient comments to preclude me from providing further descriptions.
if((nType != splitBorder && nType != splitBar) || (pDC == NULL))
{
CSplitterWnd::OnDrawSplitter(pDC, nType, rectArg);
return;
}
ASSERT_VALID(pDC);
switch (nType)
{
case splitBorder :
{
pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW),
GetSysColor(COLOR_BTNHIGHLIGHT));
}
break;
case splitBar :
{
CRect wndRect;
GetWindowRect(&wndRect);
CRect tempRect = rectArg;
pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW),
GetSysColor(COLOR_BTNHIGHLIGHT));
CFont docFont;
CFont* pOldFont;
CBrush newBrush;
CBrush* pOldBrush;
newBrush.CreateSolidBrush(RGB(255,255,0));
pOldBrush = pDC->SelectObject(&newBrush);
BuildFont(pDC, &docFont, 9, false, false);
pOldFont = pDC->SelectObject(&docFont);
CString sTitle = "INFO";
CSize sz = pDC->GetTextExtent(sTitle);
tempRect.DeflateRect(1,1,1,1);
int xPos = (int)((wndRect.Width() - sz.cx) * 0.50);
pDC->SetTextColor(RGB(0,0,0));
pDC->SetBkMode(TRANSPARENT);
pDC->Rectangle(&tempRect);
pDC->SelectObject(pOldBrush);
newBrush.DeleteObject();
bool bCanDrawText = (xPos - 30 > tempRect.left);
if (bCanDrawText)
{
pDC->TextOut(xPos, tempRect.top, sTitle);
}
pDC->SelectObject(pOldFont);
if (bCanDrawText)
{
newBrush.CreateSolidBrush(RGB(0,0,0));
pOldBrush = pDC->SelectObject(&newBrush);
tempRect.DeflateRect(0,3,0,4);
CPoint pts[4];
for (int i = 0; i <=1; i++)
{
int x = (i == 0) ? tempRect.Width() - sz.cx - 30
: tempRect.Width() + sz.cx + 15;
int y = tempRect.top + 2;
x = (int)(x * 0.50);
pts[0] = CPoint(x, y);
pts[1] = CPoint(x + 10, y);
pts[2] = CPoint(x + 5, y + 5);
pts[3] = CPoint(x, y);
pDC->Polygon(pts, 4);
}
pDC->SelectObject(pOldBrush);
newBrush.DeleteObject();
}
}
break;
}
Coding Notes |
This version of CFlatSplitterWnd only supports the drawing of horizontal splitter bars. Later in this series of articles, we'll try to fix that. |
I also added a function to build the font we need. To keep the OnDrawSplitter() function as clean as possible, I moved this code into its own function, as shown below.
BOOL CFlatSplitterWnd::BuildFont(CDC* pDC, CFont* pFont, int nFontHeight,
bool bBold, bool bItalic)
{
CString sFontName = _T("Arial");
int nWeight = (bBold) ? FW_BOLD : FW_NORMAL;
BYTE nItalic = (bItalic) ? 1 : 0;
nFontHeight = -MulDiv(nFontHeight,
pDC->GetDeviceCaps(LOGPIXELSY), 72);
return pFont->CreateFont(nFontHeight, 0, 0, 0, nWeight, nItalic, 0,
0, DEFAULT_CHARSET, OUT_CHARACTER_PRECIS,
CLIP_CHARACTER_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, sFontName);
}
- Now, we're going to add support for swappable views inside the splitter window. For this feature, I modified the CFlatSplitterWnd class to include the necessary code from another article here on CodeProject - Unlimited number of switchable views within a Splitter window, by Dan Clark.
Because we're just copy/pastng from another article into our existing splitter window class, there's no need to actually use any of the files from Dan's article. Well just talk about the functions I copy/pasted, and the changes I made to those functions. Of course, you could alternately just derive this class from CFlatSplitterWnd (or vice-versa), but I personally don't like a lot of files, not to mention taxing VS2005's ability to keep up with everything (the more files/classes you have, the longer it takes the IDE to perform certain funcitons, like updating intellisense).
The AddSwitchableView function is called from the CMainFrame OnCreateClient() function, and is called for each view you want to make swappable. When I first looked at this code, I decided there were slight improvements that could be made, so these functions don't quite match Dan's article. Changes to the original code are noted in the comments.
bool CFlatSplitterWnd::AddSwitchableView(CRuntimeClass* pView,
CCreateContext* pContext,
CRect& size,
UINT viewID)
{
CWnd* pWin = (CWnd*) pView->CreateObject();
DWORD style = WS_CHILD;
if (m_bFirstView)
{
style |= WS_VISIBLE;
m_bFirstView = false;
}
pWin->Create(NULL, NULL, style, size , this, viewID, pContext);
views[pWin] = viewID;
return true;
}
The SwitchView() function actually does the work of switching views. I changed most of the comments and mad a small change regarding the setting of the window ID for the old view.
bool CFlatSplitterWnd::SwitchView(UINT id, int paneRow, int paneCol)
{
CView* pOldView = (CView*) GetPane(paneRow, paneCol);
ASSERT(pOldView != NULL);
if (pOldView == NULL)
{
MessageBeep(0);
return false;
}
CView* pNewView = (CView*)GetDlgItem(id);
ASSERT(pNewView != NULL);
if (pNewView == NULL )
{
return false;
}
CFrameWnd* mainWnd = (CFrameWnd*)AfxGetMainWnd();
ASSERT(mainWnd != NULL);
if (mainWnd == NULL)
{
return false;
}
if (mainWnd->GetActiveView() == pOldView)
{
mainWnd->SetActiveView(pNewView);
}
pNewView->ShowWindow(SW_SHOW);
pOldView->ShowWindow(SW_HIDE);
pNewView->SetDlgCtrlID(IdFromRowCol(paneRow, paneCol));
CWnd* pCwnd =(CWnd*)pOldView;
if (views.find(pCwnd) != views.end())
{
UINT oldId = views[pCwnd];
pOldView->SetDlgCtrlID(oldId);
}
RecalcLayout();
pOldView->Invalidate();
pNewView->Invalidate();
return true;
}
The GetViewPtr() function wasn't changed from the original version. It simply retrieves a pointer to the view mapped to the specified ID.
CWnd* CFlatSplitterWnd::GetViewPtr(UINT id, int paneRow, int paneCol)
{
map<CWnd*, UINT>::iterator It, Iend = views.end();
for (It = views.begin(); It != Iend; It++)
{
if ((*It).second == id)
{
return (*It).first;
}
}
return NULL;
}
The GetIsActiveView() function was added by yours truly to assist in determining a given view's active status, and is called from CMainFrame.
BOOL CFlatSplitterWnd::GetIsActiveView(UINT nID, int nPaneRow,
int nPaneCol)
{
CWnd* pOldWnd = GetPane(nPaneRow, nPaneCol);
CWnd* pNewWnd = GetViewPtr(nID, nPaneRow, nPaneCol);
return (pOldWnd == pNewWnd);
}
The following lines were added to CFlatSplitterWnd.H:
class CFlatSplitterWnd : public CSplitterWnd
{
public:
map<CWnd*, UINT> views;
bool m_bFirstView;
CWnd* GetViewPtr (UINT id, int paneRow, int paneCol);
bool SwitchView (UINT id, int paneRow, int paneCol);
bool AddSwitchableView(CRuntimeClass* pView, CCreateContext* pContext,
CRect& size, UINT viewID);
BOOL GetIsActiveView (UINT nID, int nPaneRow, int nPaneCol);
Hook up CMainFrame
- Add an override for OnCreateClient. The class wizard will put this function at the bottom of CMainFrame:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs,
CCreateContext* pContext)
{
return CFrameWnd::OnCreateClient(lpcs, pContext);
}
Replace that function with the following version. The comments in this code block should sufficiently explain what's happening.
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs,
CCreateContext* pContext)
{
CRect cr;
GetWindowRect(&cr);
int nHeight = ::GetSystemMetrics(SM_CYSCREEN) - 100;
if (!m_mainSplitter.CreateStatic(this, 2, 1))
{
AfxMessageBox("Error setting up splitter window", MB_ICONERROR);
return FALSE;
}
m_mainSplitter.AddSwitchableView( RUNTIME_CLASS(CPrimaryView ),
pContext,
CRect(0, 1, cr.Width(), nHeight),
IDC_PANE0_PRIMARY_VIEW);
m_mainSplitter.AddSwitchableView( RUNTIME_CLASS(CSecondaryView),
pContext,
CRect(0, 1, cr.Width(), nHeight),
IDC_PANE0_SECONDARY_VIEW);
m_mainSplitter.CreateView(0, 0, RUNTIME_CLASS(CSDIMultiApp1View),
CSize(cr.Width(), nHeight), pContext);
m_mainSplitter.CreateView(1, 0, RUNTIME_CLASS(CInfoView),
CSize(cr.Width(), 0), pContext);
return TRUE;
}
- Next, we need to add some helper functions to make our life eaiser in the future.
CPrimaryView* CMainFrame::GetPrimaryView()
{
return (dynamic_cast<CPrimaryView*>(m_mainSplitter.GetPane(0,0)));
}
CSecondaryView* CMainFrame::GetSecondaryView()
{
return (dynamic_cast<CSecondaryView*>(m_mainSplitter.GetPane(0,0)));
}
CInfoView* CMainFrame::GetInfoView()
{
return (dynamic_cast<CInfoView*>(m_mainSplitter.GetPane(1,0)));
}
As you can see, these functions merely return a pointer to the active view that is indicated in the name of the function. Since we're using dynamic_cast, the pointer will be NULL if the view returned is not of the type specified between the < > symbols.
Now that we have all of our view classes coded and the splitter window implemented, we need to provide the user wth a way to switch the views.
Menus Make It Go
The user will be able to switch between the Primary View and the Secondary View via menu items. Adding menus in a MFC app is fairly trivial.
- In the IDE's Resource View, open up the resource, expand the tree until you see a list of resource categories, expand the Menu item, and double-click IDR_MAINFRAME. A new window will open in the IDE showing the program's current menu.
- Find the View Item, click it, and then right-click on the line that reads "Type Here" (just beneath the Status Bar item).
- Right click the button and select the Insert Separator item from the subsequent menu. A separator line will be added to the menu, and a new "Type Here" line will appear below the separator.
- Click on the new Type Here item, and type in "Primary View". Notice that a new "Type Here" item is added directly below.
- Click on the new Type Here item, and type in "Secondary View".
- Go back and right-click on the Primary View item and select Add Event Handler... from the menu.
- The Event Handler Wizard dialog box should now be displayed (see below). This dialog allows you to set one event handler at a time. Notice there are two possible events we can set. We need them both for the sample but since we can only do one at a time, we'll pick ON_COMMAND. Make double-damn sure that you've selected the correct class to which we are adding this handler - it MUST be CMainFrame. You'll have to repeat the previous step to get back here to set a handler for ON_COMMAND_UI. Again, make sure that you add the handler to the correct class.
- Add code to the new message handling functions to make them switch views and enable/disable the menu items depending on which view is currently active.
void CMainFrame::OnViewPrimaryview()
{
m_mainSplitter.SwitchView(IDC_PANE0_PRIMARY_VIEW, 0, 0);
}
void CMainFrame::OnUpdateViewPrimaryview(CCmdUI *pCmdUI)
{
pCmdUI->Enable(!m_mainSplitter.GetIsActiveView(
IDC_PANE0_PRIMARY_VIEW,0,0));
}
void CMainFrame::OnViewSecondaryview()
{
m_mainSplitter.SwitchView(IDC_PANE0_SECONDARY_VIEW, 0, 0);
}
void CMainFrame::OnUpdateViewSecondaryview(CCmdUI *pCmdUI)
{
pCmdUI->Enable(!m_mainSplitter.GetIsActiveView(
IDC_PANE0_SECONDARY_VIEW,0,0));
}
Compile The App
Rebuild the solution, and run the app. The 1st image shows the app the way it's initally displayed. The 2nd screenshot shows what the app looks like when you click/drag the splitter bar. The 3rd image illustrates the menu selection of the Secondary view, and the 4th image shows the secondary view.
What's Next?
In Part 3, we'll be adding a custom status bar class and some multi-threading components, again using articles found here on CodeProject.
End of Part 2
Due to the length of this article, I've decided to break it up into several parts. If the site editors did what I asked, all of the subsequent parts should be in the same code section of the site. Each part has it's own source code, so as you read the subsequent parts, make sure you download the source code for that part (unless you're doing manually all the stuff I'm outlining in the article in question).
In the interest of maintaining some cohesiveness (and sanity), please vote on all of the parts, and vote the same way. This helps keep the articles together in the section. Thanks for understanding.