Introduction
This is a bug-tracking application for use within a software company that most of the people in my company have been using for a couple of months now.
Background
I started this project as a way to keep track of our company's bugs and communicate back and forth between the testers and programmers on our LAN. We already had a web-based bug-tracking tool, but nobody was actually using it. Besides, I don't really like web-based software for this purpose. So I've worked on this over a few months' time, and I'm pretty proud of it. I know there're still a couple of bugs in it, and also some stuff that could be changed to make it more efficient (which I will discuss below in the appropriate section).
First of all, I'd foremost like to thank Chris Maunder and the other CPians who have worked on the MFC Grid Control. This control made things a lot easier for what I was trying to do. I thank Mingming Lu, the author of the 'Network renju Game' article for the code I got for dealing with sockets. I also thank Prateek Kaul, author of the 'System Tray Icons' article for the class that I use in my program for displaying the icon in the system tray. Last, but definitely not least, I'd like to thank the Marc Richarme, author of the 'EasySize'() article. His macros have been invaluable in the resizing of my dialogs.
This code was compiled using Visual Studio .NET 2003, using version 7.1 of the MFC, and works fine under Windows2000/XP.
(Important!!!) Please Read This First
This code uses an SQL Server database on our company's server. I have included a script (bugreporter.sql) to recreate all the tables for your own database. There is an entry in our database for '(unknown)' which happens to have a personid
of 4 in our database. Some of the code actually references this constant value, that's why I've added to the script to add these people. So don't delete this record -- or change this value accordingly. There have also been values added to the tables 'tlkpreturnstatus' and 'tlkptype'. There are five values in the table 'tlkpSeverity'. These values correspond to the flag icons that show up in the grid (and there're only five of them).
I have not included Chris Maunder's MFC Grid source, because the graphics and icons I use for this project are big enough as it is. I also have not included the CSystemTray
source files, or the EasySize source files. Besides making the download bigger, I think all these articles should be downloaded by the user so that I'm not redistributing another author's source code. Also, I think these are all good articles, so you should probably give them a look yourself!
If you run the demo, you will need to run the SQL script first in a database that you create on your SQL Server, with database, username, and passwords of 'bugreporter'. The demo was built as 'SHOWSERVERCONFIGRELEASE' so that you are allowed to input the name of your server. If you use this code for your company, then you could put the appropriate values in and use the regular 'RELEASE' version. But it will only ask you for the server name once either way, and stores the value in the registry. The first time you log in to BugReporter, you can use 'fp','sp', or 'tp' as login names.
I've used ADO in this application by using the #import
directive. The path to this file may be different on your computer.
#import "C:\Program Files\Common Files\System\ado\msado15.dll" \
no_namespace \
rename("EOF", "adoEOF")
Using the code
Once you have the application running, go to the 'Tools' menu, and select 'Maintenance' or click the gear icon on the toolbar. You will need to set up a project to put any bugs into the application. Once you add a new project, be sure to add a version number! Since the maintenance is probably going to be done by the programmers, I haven't included a lot of functionality to this screen. However, it should not allow you to delete a project or user if they're tied to a bug already. Also, since this app is used on our LAN, I use the 'computername' field to keep track of the users' computer name for sending alerts and chatting.
To add a new bug report, click on 'New'. This puts the user in 'NEW
' state. (The 'state
' is an enum
property used for enabling/disabling of controls, and to determine how to save a bug if it is in 'EDIT
' or 'NEW
' mode.) Some type of description is required, as well as the 'Submitted By', 'Submitted To', and 'Version'.
To update a bug, (usually done by the person who submitted the bug in the first place), click the 'Edit' button. This puts you in 'EDIT
' mode. You can now change any of the parameters of the bug submittal.
When a programmer wishes to respond to a bug, press 'Respond'. This will enable the 'Return Status Notes' edit box, and the 'Return Status' combo.
If there's a bug selected, you can add an attachment by dragging an item onto the application in any mode, or clicking the 'Plus' icon next to the attachments listbox in 'NEW
' or 'EDIT
' mode. When you do this, the actual bytes of the file are saved in the database, along with the filename and extension. To view an attachment, double-click on it in the listbox. The file is re-created from the database and saved in a folder called 'temp' in the same folder where BugReporter is located. I then use ShellExecute()
to open the file with the default application. The files are marked as Read-Only so that if you open it and then forget it was opened from BugReporter, it won't allow you to save over the copy in the 'Temp' folder. This folder is cleaned out upon exiting BugReporter, so make sure you don't save anything else in this folder!! You can remove attachments from a bug by clicking the 'Minus' sign next to the listbox. Here's what that code looks like:
void CBugReporterView::OnLbnDblclkScaps()
{
CString url = _T("");
int index = m_lstScaps.GetCurSel();
m_lstScaps.GetText(index, url);
int data = (int)m_lstScaps.GetItemData(index);
CString strMessage,strWhere,strDirectory;
char szPath[512];
BYTE* pBuf;
BYTE* pBytes;
_variant_t varBLOB;
unsigned long nLength;
try
{
BeginWaitCursor();
_RecordsetPtr rst;
strWhere.Format("LinkID = %d",data);
rst = theApp.GetRecordset(_T("*"),_T("tblLink") ,strWhere);
CString str,strPath;
FieldsPtr pFields = rst->GetFields();
FieldPtr pField,pName;
pField= pFields->GetItem(_T("FileBytes"));
pName = pFields->GetItem(_T("Link"));
nLength = pField->GetActualSize();
str.Format("%s",(char*)_bstr_t(pName->Value));
varBLOB = pField->GetChunk(nLength);
SafeArrayAccessData(varBLOB.parray,(void **)&pBuf);
pBytes = pBuf;
SafeArrayUnaccessData(varBLOB.parray);
::GetModuleFileName(NULL,szPath,512);
strPath.Format("%s",szPath);
int pos = strPath.ReverseFind('\\');
strDirectory = strPath.Left(pos + 1) + "temp\\";
strPath = strDirectory + url;
CreateDirectory(strDirectory,NULL);
CFile file;
CFileException ex;
CFileStatus status;
status.m_attribute = 0;
//if file already exists in this directory,
//remove read-only attribute, then delete it
if(file.Open(strPath,CFile::modeRead,&ex))
{
file.Close();
CFile::SetStatus(strPath,status);
DeleteFile(strPath);
}
//if we're able to create the file
if(file.Open(strPath,CFile::modeCreate | CFile::modeWrite,&ex))
{
file.Write(pBytes,nLength);
file.Close();
file.GetStatus(status);
status.m_attribute |= 0x01; //read only
CFile::SetStatus(strPath,status);
int iReturn = (int) ShellExecute(NULL, _T("open"),
strPath, NULL, NULL, SW_SHOWNORMAL);
// If ShellExecute returns an error code, let the user know.
if (iReturn <= 32)
MessageBox (_T("Cannot open file. File may have"
" been moved or deleted."), _T("Error!"),
MB_OK | MB_ICONEXCLAMATION) ;
if(url.GetLength()==0)
MessageBox (_T("This is an empty entry!"
" Try clicking a filled link! "),
_T("Error!"), MB_OK | MB_ICONEXCLAMATION) ;
}//if(file.Open(strPath,CFile::modeCreate | CFile::modeWrite,&ex))
else //file might have been opened already
{
AfxMessageBox("File could not be opened from this location.\r\n"
"Check to make sure you don\'t already"
" have this attachment open.",MB_ICONINFORMATION);
}
}
MYCATCHALL
VariantClear(&varBLOB);
url.ReleaseBuffer();
EndWaitCursor();
}
After a bug has been marked as 'fixed' or 'irreproducible' for return status, it is deemed 'Ready to Test', which you can see by changing the current view with the combobox on the upper right of the application under 'Ready to Test'. If the submitter of the bug re-tests the bug and sees that it is resolved, they should mark it as such. If not, the user can add more text to the description and save. When saving, it will ask if you want to mark the bug as 'Return to Programmer'. If so, the bug will show up with its 'type' column highlighted in yellow so that it stands out to the programmer.
The 'Current View' combo is populated from all the types in the table 'tlkptype' in the database. I have included some default types in the SQL script to populate this table, as well as some for the 'Return Status'. (I think this is probably already too many, so you probably won't have to add anymore.) There are also hard-coded values that are added to the combobox, like 'Ready To Test', 'Mine', and have itemdata associated with them in the combobox to generate the appropriate SQL statement in the Refresh()
procedure. These are the other types added to 'Current View' combobox, and the SQL generated from them:
switch(nFilter)
{
case 1000:
strWhere2.Format(_T(" AND dbo.tblBug."
"SubmittedToID = %d"),theApp.m_nUserID );
break;
case 1001:
strWhere2 = _T(" AND dbo.tblBug.Resolved = 0");
break;
case 1002:
strWhere2 = _T(" AND dbo.tblBug.ReturnStatusID = 4");
break;
case 1003:
strWhere2 = _T(" AND dbo.tblBug.Resolved = 1");
break;
case 1004:
strWhere2 = _T("");
break;
case 1005:
strWhere2 = _T(" AND ((dbo.tblBug.Resolved = 0 AND
dbo.tblBug.ReturnStatusID = 2) OR
(dbo.tblBug.Resolved = 0 AND
dbo.tblBug.ReturnStatusID = 1))");
break;
case 1006:
strWhere2 = _T(" AND dbo.tblBug.RTP = 1");
break;
default:
strWhere2.Format(_T(" AND dbo.tblBug.TypeID = %d"),nFilter);
}
After saving/editing/responding to a bug, you will also get a dialog (unless you have turned it off through the 'Options' screen) asking if you would like to send the appropriate person an alert. This will display a brief message box that will tell you if your alert was sent successfully or not. You may also send a user an alert at any time by clicking the button next to their name in 'Submitted By' or 'Submitted To' comboboxes. You cannot send an alert to yourself, or to 'UNKNOWN', or to any user with 'COMPUTER' set as their computername. When you receive an alert, you will receive a messagebox, and after clicking 'OK', you will be set to the appropriate bug.
There are several options you can configure here, and I think most of them are self-explanatory. Just make sure that you click 'Apply' after changing any options or they won't be saved.
This screen populates all the users in your system except you, of course. Just select which user you want to chat with, and send them a message. Your message won't go through if the user doesn't have BugReporter running or if they have put themselves in 'Do not Disturb' mode (found on the 'Tools' menu and indicated by a checkmark). Every message sent opens and closes the chat socket, so you can chat with multiple people at once. If you're chatting with Person1 and receives a message from Person2, it puts Person2 in the SendTo combo so that your next messge goes to Person2, unless you're in the middle of typing. In that case, it won't switch the person, because I'd assumed that if you were already typing, then you want your current message to go to the person you've already selected. I implemented a homemade encryption algorithm to hide your text from sniffers. It's no Computer Science winner, it was just something I wanted to try. :)
Chris Maunder's MFC Grid already takes care of the printing with or without the Doc/View architecture, but I added a way to print a single bug at a time so that you can see the entire 'Description', 'Response', and any attachments (the filenames are listed under 'Attachments:'). If your current view is 'Ready to Test', it will prompt you for a 'Version Number'. This is not necessary, it's just a way to display a text value on the report for the tester's reference.
The 'Find' Dialog allows you to put in different criteria to find previously submitted bugs. You can also print the list once you have one. It might not be that helpful, but it's free, since all I had to do was call the grid's Print()
function! After you locate a bug, you can double-click it to view it on the main screen. This dialog is modeless, so you can switch back and forth as necessary.
Points of Interest
Again, using the MFC Grid's built-in functionality, I've allowed the user to save the current list as a comma-delimited text file, in case they want to export it to somewhere else. I've used a pretty generic error handler that writes out the errors in HTML format. I've defined this as MYCATCHALL
in the stdafx.h file:
#define MYCATCHALL catch(CException* e) \
{ \
m_pErrorHandler->HandleError(e,__FUNCTION__); \
} \
catch(_com_error e) \
{ \
m_pErrorHandler->HandleError(e,__FUNCTION__); \
} \
catch(...) \
{\
m_pErrorHandler->HandleError(__FUNCTION__);\
}
Most of the dialogs have animation (which you can turn off in the 'Options' dialog). I made a function for the application class to handle this. Just send it the HWND
of the dialog you want to animate, and it randomly picks one. I removed the Fade In/Out animation because controls weren't painting correctly.
void CBugReporterApp::AnimateDialog(HWND hWnd)
{
DWORD dwTemp2,dwTemp,dwAnimate = AW_ACTIVATE;
int nFirst,nSecond;
try
{
srand( (unsigned)time( NULL ) );
nFirst = rand() % 3 + 1;
switch(nFirst)
{
case 1: dwTemp = AW_SLIDE; break;
case 2:
case 3: dwTemp = AW_CENTER; break;
}
dwAnimate |= dwTemp;
if(nFirst == 1)
{
srand( (unsigned)time( NULL ) );
nSecond = rand() % 4 + 1;
switch(nSecond)
{
case 1: dwTemp2 = AW_HOR_POSITIVE; break;
case 2: dwTemp2 = AW_HOR_NEGATIVE; break;
case 3: dwTemp2 = AW_VER_POSITIVE; break;
default:dwTemp2 = AW_VER_NEGATIVE;
}
dwAnimate |= dwTemp2;
srand( (unsigned)time( NULL ) );
nSecond = rand() % 2;
if(dwTemp2 == AW_VER_POSITIVE || dwTemp2 == AW_VER_NEGATIVE)
{
dwTemp2 = 0;
switch(nSecond)
{
case 1: dwTemp2 = AW_HOR_POSITIVE; break;
case 2: dwTemp2 = AW_HOR_NEGATIVE; break;
}
dwAnimate |= dwTemp2;
}
else
{
dwTemp2 = 0;
switch(nSecond)
{
case 1: dwTemp2 = AW_VER_POSITIVE; break;
case 2: dwTemp2 = AW_VER_NEGATIVE; break;
}
dwAnimate |= dwTemp2;
}
}
::AnimateWindow(hWnd,200,dwAnimate);
}
MYCATCHALL
}
There are also a few helper functions that I use for dealing with recordsets and comboboxes.
void CBugReporterApp::CloseRecordset(_RecordsetPtr rst)
int CBugReporterApp::GetIntField(CString strSQL)
void CBugReporterApp::FillCombo(CString strTable, CString strDataField,
CString strDisplayField, CComboBox& cbo,
CString strAlias,BOOL bAddEmpty)
CString CBugReporterApp::GetStringField(CString strField,
CString strTable, CString strWhere)
void CBugReporterApp::ExecuteSQL(CString strSQL)
void CBugReporterApp::SetComboFromID(CComboBox& cbo, int nID)
void CBugReporterApp::EmptyCombo(CComboBox& cbo)
_RecordsetPtr CBugReporterApp::GetRecordset(CString strFields,
CString strTable, CString strWhere)
The 'Zoom' dialog, the main app window, and most others have a value that will be saved in the registry for their placement so that their positions and sizes are persistent. I took this from an example I found somewhere. This is what it looks like for the Chat dialog:
void CChatDlg::OnDestroy()
{
CDialog::OnDestroy();
WINDOWPLACEMENT wp;
GetWindowPlacement(&wp);
theApp.WriteProfileBinary("Settings", "ChatWindowPos",
(LPBYTE)&wp,sizeof(wp));
}
I made the toolbar buttons bigger with (I think) better icons.
HBITMAP hbm = (HBITMAP)::LoadImage(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDB_BITMAP1), IMAGE_BITMAP, 0,0,
LR_CREATEDIBSECTION | LR_LOADMAP3DCOLORS );
CBitmap bm;
bm.Attach(hbm);
int ret = m_ilToolBar.Create(32,32,ILC_COLOR8 | ILC_MASK,1,0);
ret = m_ilToolBar.Add(&bm,RGB(0,0,0));
m_wndToolBar.GetToolBarCtrl().SetImageList(&m_ilToolBar);
SIZE szButton,szImage;
szButton.cx = 39;
szButton.cy = 38;
szImage.cx = 32;
szImage.cy = 32;
m_wndToolBar.SetSizes(szButton,szImage);
m_wndToolBar.GetToolBarCtrl().AutoSize();
I also added a popup menu for when you right-click on the grid in the main window. You can either delete the selected bug(s), or change the project for the selected bug(s). Once you open the popup menu, the secondary menu for the projects shows all the projects except the current one. This is done dynamically every time you view the menu and is implemented in the subclassed grid control I made for the main window, called CMyGridCtrl
:
void CMyGridCtrl::OnRButtonUp(UINT nFlags, CPoint point)
{
if(this->GetRowCount() > 1)
{
_RecordsetPtr rst;
CString strSQL;
CPoint mypoint(point);
ClientToScreen(&mypoint);
CMenu* pMenu = this->pPopupMenu->GetSubMenu(0);
CMenu* pMenu2 = pMenu->GetSubMenu(0);
while(pMenu2->RemoveMenu(0,MF_BYPOSITION));
strSQL.Format(_T("SELECT ProjectID,Project FROM tblProject"
" WHERE ProjectID <> %d"),this->m_pView->m_nCurrentProjectID);
VARIANT* var = NULL;
rst.CreateInstance(__uuidof (Recordset));
rst->CursorLocation = adUseClient;
rst = theApp.g_Cnn->Execute(strSQL.AllocSysString(),var,0L);
FieldsPtr pFields;
FieldPtr pID,pName;
if(!rst->adoEOF)
pFields = rst->Fields;
int nStatusID = 0;
CString strStatus;
while(!(rst->adoEOF))
{
pID = pFields->GetItem(_T("ProjectID"));
pName = pFields->GetItem(_T("Project"));
nStatusID = (int)pID->Value;
strStatus.Format("%s",(char*)_bstr_t(pName->Value));
pMenu2->AppendMenu(MF_STRING,nStatusID,strStatus);
rst->MoveNext();
}
pMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
mypoint.x,mypoint.y,this);
theApp.CloseRecordset(rst);
}
CGridCtrl::OnRButtonUp(nFlags, point);
}
The magnifying glass buttons zoom you in so you you have more viewing space when viewing/editing bugs. The last project, bug, view are all saved in the registry so that the application puts you right back where you were when starting the app again. 'BUGID' shows up in the grid so that when printing out a list, you can easily identify a bug by its number if the description is too short.
Known Problems
- If app is open overnight, it sometimes loses connection and has to be restarted.
- Should use
CString
variable when sending chat messages.
- My copy of the MFC Grid Control still had a bug in the
EnsureVisible()
function, see the fix I implemented here. If this bug is still in the source when you compile it yourself, it will crash when calling EnsureVisible()
in BugReporter's Refresh()
function if the grid doesn't have focus.
Closing
I hope this app helps anybody and really hope you rate it. I'm pretty sure there are probably other bugs that I haven't found yet, but I'd be happy to know about them and will change the code accordingly. Whether you like the article or not, please rate it. If you don't like it and give the article a low rating, I hope that you'll also leave a comment so that I know why.
Feel free to use this code in any way you like. Any additions and/or changes are welcome.
History
- Version 4.2 - Posted 01/26/2005.