65.9K
CodeProject 正在变化。 阅读更多。
Home

Bugreporter

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (36投票s)

2005年1月27日

11分钟阅读

viewsIcon

148494

downloadIcon

2918

一款方便的软件开发公司使用的 Bug 跟踪工具。

Main Screen

引言

这是一款用于软件公司的 Bug 跟踪应用程序,我公司的大多数人现在已经使用它几个月了。

背景

我启动这个项目是为了跟踪我们公司的 Bug,并在我们 LAN 上的测试人员和程序员之间进行沟通。我们已经有一个基于 Web 的 Bug 跟踪工具,但没有人真正使用它。而且,我其实不太喜欢为此目的使用基于 Web 的软件。因此,我在几个月的时间里一直在开发这个项目,我对此感到很自豪。我知道里面还有一些 Bug,还有一些地方可以改进使其更有效率(我将在下面的相应部分讨论)。

首先,我最想感谢 Chris Maunder 和其他贡献者对 MFC Grid Control 的贡献。这个控件使我想要做的事情变得容易了许多。我感谢 Mingming Lu,他是我获取处理套接字代码的 “网络围棋游戏” 文章的作者。我还感谢 Prateek Kaul,他是 'System Tray Icons' 文章的作者,我从他的文章中获取了用于在系统托盘中显示图标的类。最后,但同样重要的是,我想感谢 Marc Richarme,他是 “EasySize”() 文章的作者。他的宏对我对话框的重绘非常有帮助。

这段代码是使用 Visual Studio .NET 2003 编译的,使用的是 MFC 7.1 版本,并且在 Windows 2000/XP 下运行良好。

(重要!!!) 请先阅读

这段代码使用我们公司服务器上的 SQL Server 数据库。我包含了一个脚本 (bugreporter.sql) 来为您自己的数据库重新创建所有表。我们的数据库中有一个条目名为“(未知)”,其 personid 在我们的数据库中恰好为 4。部分代码实际上引用了这个常量值,这就是为什么我在脚本中添加了这些人的条目。所以请不要删除此记录 — — 或者相应地更改此值。另外,在 'tlkpreturnstatus' 和 'tlkptype' 表中也添加了值。'tlkpSeverity' 表中有五个值。这些值对应于网格中显示的标志图标(它们只有五个)。

我没有包含 Chris Maunder 的 MFC Grid 源代码,因为这个项目我使用的图形和图标已经够大了。我也没包含 CSystemTray 源代码文件,或者 EasySize 源代码文件。除了使下载文件变大之外,我认为所有这些文章都应该由用户自己下载,这样我就不会重新分发其他作者的源代码了。另外,我认为这些都是很好的文章,所以您应该自己看看!

如果您运行演示版,您需要先在您创建的 SQL Server 数据库中运行 SQL 脚本,数据库、用户名和密码均为 'bugreporter'。演示版是作为 'SHOWSERVERCONFIGRELEASE' 构建的,这样您就可以输入服务器名称。如果您将此代码用于您的公司,那么您可以输入相应的值并使用常规的 'RELEASE' 版本。但无论如何,它只会问您一次服务器名称,并将其值存储在注册表中。首次登录 BugReporter 时,您可以使用 'fp'、'sp' 或 'tp' 作为登录名。

我通过使用 `#import` 指令在应用程序中使用了 ADO。该文件在您计算机上的路径可能不同。

#import "C:\Program Files\Common Files\System\ado\msado15.dll" \
no_namespace \
rename("EOF", "adoEOF")

使用代码

Maintenance Screen

一旦应用程序运行起来,请转到“工具”菜单,选择“维护”,或单击工具栏上的齿轮图标。您需要设置一个项目才能将任何 Bug 输入到应用程序中。添加新项目后,请务必添加版本号!由于维护很可能由程序员完成,所以我没有在此屏幕上添加很多功能。但是,它不应允许您删除已与 Bug 关联的项目或用户。此外,由于此应用程序在我们公司的 LAN 上使用,我使用 'computername' 字段来跟踪用户的计算机名,以便发送警报和聊天。

要添加新的 Bug 报告,请单击“新建”。这将使用户处于“新建”状态。('状态' 是一个 `enum` 属性,用于启用/禁用控件,并确定如何保存 Bug(如果处于“`编辑`”或“新建”模式)。需要某种描述,以及“提交人”、“提交给”和“版本”。

要更新 Bug(通常由最初提交 Bug 的人完成),请单击“编辑”按钮。这将使您进入“`编辑`”模式。您现在可以更改 Bug 提交的任何参数。

当程序员希望响应 Bug 时,请按“响应”。这将启用“返回状态说明”编辑框和“返回状态”组合框。

如果选中了一个 Bug,您可以通过将一个项目拖到应用程序上的任何模式下,或者在“`新建`”或“`编辑`”模式下单击附件列表框旁边的“加号”图标来添加附件。这样做时,文件的实际字节将与文件名和扩展名一起保存在数据库中。要查看附件,请在列表框中双击它。该文件将从数据库中重新创建,并保存在 BugReporter 所在文件夹中的一个名为“temp”的文件夹中。然后我使用 `ShellExecute()` 来用默认应用程序打开文件。文件被标记为只读,因此如果您打开它,然后忘记了它是从 BugReporter 打开的,它将不允许您覆盖“temp”文件夹中的副本。退出 BugReporter 时会清除此文件夹,因此请确保不要在此文件夹中保存任何其他内容!您可以通过单击列表框旁边的“减号”来从 Bug 中删除附件。以下是相关代码:

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);
        //pBuf = new BYTE[nLength];

        SafeArrayAccessData(varBLOB.parray,(void **)&pBuf);
        // Initialize the data member variable with the data.

        pBytes = pBuf;
        // Unlock the safe array data.

        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();

}

将 Bug 标记为“已修复”或“无法重现”作为返回状态后,它将被视为“准备测试”,您可以通过更改应用程序右上角“准备测试”下的组合框来查看。如果 Bug 的提交者重新测试了 Bug 并发现它已解决,他们应该将其标记为此。如果没有,用户可以向描述中添加更多文本并保存。保存时,会询问您是否要将 Bug 标记为“返回程序员”。如果是,Bug 将以其“类型”列高亮显示为黄色,以便程序员一眼就能看到。

“当前视图”组合框是从数据库中的 'tlkptype' 表中的所有类型填充的。我在 SQL 脚本中包含了一些默认类型来填充此表,以及一些用于“返回状态”的类型。(我认为这可能已经太多了,所以您可能不需要添加更多。)组合框中还添加了一些硬编码的值,如“准备测试”、“我的”,并与组合框中的项目数据关联,以在 `Refresh()` 过程中生成相应的 SQL 语句。这些是添加到“当前视图”组合框的其他类型,以及从中生成的 SQL:

        switch(nFilter)
        {
        case 1000:
            //only the current user's bugs

            strWhere2.Format(_T(" AND dbo.tblBug." 
                     "SubmittedToID = %d"),theApp.m_nUserID );
            break;
        case 1001:
            //only unresolved bugs

            strWhere2 = _T(" AND dbo.tblBug.Resolved = 0");
            break;
        case 1002:
            //only unanswered bugs

            strWhere2 = _T(" AND dbo.tblBug.ReturnStatusID = 4");
            break;
        case 1003:
            strWhere2 = _T(" AND dbo.tblBug.Resolved = 1");
            break;
        case 1004:
            //all

            strWhere2 = _T("");
            break;
        case 1005:
            //ready to test

            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:
            //returned to programmer

            strWhere2 = _T(" AND dbo.tblBug.RTP = 1");
            break;
        default:
            //otherwise, it's just a bug type id

            strWhere2.Format(_T(" AND dbo.tblBug.TypeID = %d"),nFilter);
        }

在保存/编辑/响应 Bug 后,您还会收到一个对话框(除非您在“选项”屏幕中将其关闭),询问您是否希望向相应的人发送警报。这将显示一个简短的消息框,告诉您警报是否发送成功。您也可以随时通过单击“提交人”或“提交给”组合框旁边的按钮向用户发送警报。您不能向自己或“未知”用户发送警报,也不能发送给计算机名设置为“COMPUTER”的任何用户。收到警报时,您会收到一个消息框,单击“确定”后,您将被转到相应的 Bug。

Options Screen

您可以在此处配置几个选项,我认为其中大多数都是不言而喻的。只需确保在更改任何选项后单击“应用”,否则它们不会被保存。

Chat Screen

此屏幕将显示您系统中的所有用户(当然,除了您自己)。只需选择您想与之聊天的人,然后发送消息。如果用户没有运行 BugReporter,或者他们将自己设置为“请勿打扰”模式(可在“工具”菜单中找到,并用复选标记表示),则您的消息将无法发送。每条消息发送都会打开和关闭聊天套接字,因此您可以同时与多人聊天。如果您正在与 Person1 聊天并收到 Person2 的消息,则会将 Person2 放入 SendTo 组合框,以便您的下一条消息发送给 Person2,除非您正在键入中。在这种情况下,它不会切换用户,因为我假定如果您已经键入,那么您希望将当前消息发送给您已选择的用户。我实现了一个自制的加密算法来隐藏您的文本免受嗅探器的侵害。它不是计算机科学的杰作,只是我想尝试的东西。 :)

Print Selected Bug

Chris Maunder 的 MFC Grid 已经处理了无论是否使用 Doc/View 架构的打印问题,但我增加了一种一次打印单个 Bug 的方法,这样您就可以看到完整的“描述”、“响应”以及任何附件(文件名列在“附件:”下)。如果您的当前视图是“准备测试”,它会提示您输入“版本号”。这不是必需的,只是在报告中显示一个文本值供测试人员参考。

Find Screen

“查找”对话框允许您输入不同的条件来查找先前提交的 Bug。您还可以一次打印列表。它可能没那么有用,但它是免费的,因为我所要做的就是调用网格的 `Print()` 函数!找到 Bug 后,您可以双击它在主屏幕上查看。此对话框是无模态的,因此您可以根据需要来回切换。

关注点

同样,使用 MFC Grid 的内置功能,我允许用户将当前列表保存为逗号分隔的文本文件,以便他们可以将其导出到其他地方。我使用了一个相当通用的错误处理程序,它以 HTML 格式写入错误。我在 stdafx.h 文件中将其定义为 `MYCATCHALL`。

#define MYCATCHALL catch(CException* e) \
{ \
    m_pErrorHandler->HandleError(e,__FUNCTION__); \
} \
catch(_com_error e)  \
{ \
    m_pErrorHandler->HandleError(e,__FUNCTION__); \
} \
catch(...) \
{\
    m_pErrorHandler->HandleError(__FUNCTION__);\
}

大多数对话框都有动画效果(您可以在“选项”对话框中关闭它)。我为应用程序类创建了一个函数来处理它。只需将您想要动画化的对话框的 `HWND` 发送给它,它就会随机选择一个。我删除了淡入/淡出动画,因为控件没有正确绘制。

void CBugReporterApp::AnimateDialog(HWND hWnd)
{
    DWORD        dwTemp2,dwTemp,dwAnimate = AW_ACTIVATE;
    int            nFirst,nSecond;

    try
    {
        //seed randomNumber Generator

        srand( (unsigned)time( NULL ) );
        nFirst = rand() % 3 + 1;

        switch(nFirst)
        {
            case 1: dwTemp = AW_SLIDE; break;
            case 2:
               //removed because it was AW_BLEND which 

               //doesn't seem to repaint controls correctly

            case 3: dwTemp = AW_CENTER; break;
        }

        dwAnimate |= dwTemp;

        //these next values will only work with AW_SLIDE

        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;
            }//if(dwTemp2 == AW_VER_POSITIVE || dwTemp2 == AW_VER_NEGATIVE)

            else
            {
                dwTemp2 = 0;
                switch(nSecond)
                {
                    case 1: dwTemp2 = AW_VER_POSITIVE; break;
                    case 2: dwTemp2 = AW_VER_NEGATIVE; break;
                }
                dwAnimate |= dwTemp2;
            }



        }//if(nFirst == 1)


        ::AnimateWindow(hWnd,200,dwAnimate);
    }
    MYCATCHALL

}

还有几个我用来处理记录集和组合框的辅助函数。

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)

“缩放”对话框、主应用程序窗口以及大多数其他窗口都有一个值会保存在注册表中,以便它们的放置位置和大小保持不变。我从某个地方找到的一个示例中获得了这个功能。以下是聊天对话框的外观:

void CChatDlg::OnDestroy()
{
    CDialog::OnDestroy();
    WINDOWPLACEMENT wp;
    GetWindowPlacement(&wp);
    theApp.WriteProfileBinary("Settings", "ChatWindowPos", 
                                  (LPBYTE)&wp,sizeof(wp));
}

我将工具栏按钮做得更大,(我认为)图标也更好。

    //create an HBITMAP for the image

    HBITMAP hbm = (HBITMAP)::LoadImage(AfxGetInstanceHandle(), 
             MAKEINTRESOURCE(IDB_BITMAP1), IMAGE_BITMAP, 0,0, // cx, cy

    LR_CREATEDIBSECTION | LR_LOADMAP3DCOLORS );

    //make a CBitmap and attach HBITMAP to it

    CBitmap bm;
    bm.Attach(hbm);


    //create imagelist and specify 256 colors

    int ret = m_ilToolBar.Create(32,32,ILC_COLOR8 | ILC_MASK,1,0);
    ret = m_ilToolBar.Add(&bm,RGB(0,0,0));
    //The second parameter sets the gray background color to transparent


    m_wndToolBar.GetToolBarCtrl().SetImageList(&m_ilToolBar);

    //make toolbar buttons the appropriate size

    SIZE szButton,szImage;
    szButton.cx = 39; //button width must be 7 pixels greater than image

    szButton.cy = 38; //height must be 6 pixels greater than image

    szImage.cx = 32;
    szImage.cy = 32;
    m_wndToolBar.SetSizes(szButton,szImage);

    //should be called after any resizing is done to buttons or images

    m_wndToolBar.GetToolBarCtrl().AutoSize();

我还添加了一个弹出菜单,用于在主窗口中的网格上右键单击。您可以删除选定的 Bug,或者更改选定 Bug 的项目。打开弹出菜单后,项目的二级菜单将显示当前项目以外的所有项目。这会动态地每次查看菜单时生成,并在我为我的主窗口制作的子类化网格控件(称为 `CMyGridCtrl`)中实现。

void CMyGridCtrl::OnRButtonUp(UINT nFlags, CPoint point)
{
    if(this->GetRowCount() > 1)
    {
        _RecordsetPtr    rst;
        CString            strSQL;
        CPoint            mypoint(point);

        ClientToScreen(&mypoint);
        //convert to screen coordinates to get point to display popupmenu


        CMenu* pMenu = this->pPopupMenu->GetSubMenu(0);

        CMenu* pMenu2 = pMenu->GetSubMenu(0);
        //remove all items from the 'Projects' submenu

        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;

        //loop through and get all the projects except 

        //the current one, then add them to submenu

        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);
}

放大镜按钮可以放大,这样您在查看/编辑 Bug 时有更多的显示空间。最后一个项目、Bug、视图都会保存在注册表中,以便应用程序在重新启动时将您带回到之前的位置。“BUGID”会显示在网格中,以便在打印列表时,您可以轻松地通过编号识别 Bug,即使描述太短。

已知问题

  • 如果应用程序在夜间打开,有时会失去连接,需要重新启动。
  • 发送聊天消息时应使用 `CString` 变量。
  • 我的 MFC Grid Control 副本在 `EnsureVisible()` 函数中仍然有一个 Bug,请在此处查看我实现的修复 这里。如果此 Bug 在您自己编译源代码时仍然存在,那么在 BugReporter 的 `Refresh()` 函数中调用 `EnsureVisible()` 时,如果网格没有焦点,程序将会崩溃。

闭运算

我希望这个应用程序能帮助到任何人,我真心希望您能给它评分。我几乎可以肯定还有一些我还没有发现的 Bug,但我很乐意知道它们,并将相应地更改代码。无论您是否喜欢这篇文章,请给它评分。如果您不喜欢它并给它低评分,我希望您也能留下评论,以便我知道原因。

请随意以任何方式使用此代码。欢迎任何添加和/或更改。

历史

  • 版本 4.2 - 发布日期 2005/01/26。
© . All rights reserved.