问题跟踪器






4.71/5 (18投票s)
今天发现了什么 bug 吗?
注意:必须在启动应用程序之前运行 MySQL 脚本!
引言
最近我发现了一个很棒的基于 Web 的无痛 Bug 跟踪工具。但在此之前,我发现它缺少一些功能,例如:
- 发送通知电子邮件的能力(如果找到解决方案)
- 附件(任何类型)的能力,例如屏幕截图等
- 记录 Bug 之间交互的能力(依赖关系)
- 开发为独立的 Windows 二进制应用程序
对于我的某些任务,ToDoList 和 BugReporter 项目是一个非常好的替代方案;但我需要一个更具可伸缩性、分布式解决方案……
使用软件
您是否运行了 MySQL 脚本来创建数据库?那么是时候建立与数据库的第一个连接了。您需要输入服务器名称、数据库名称、用户名和密码才能访问数据库,如下图所示:(请确保您拥有在“问题跟踪器”数据库表中应用 INSERT
、UPDATE
和 DELETE
语句的适当权限)
每次启动“问题跟踪器”应用程序时,它都会使用 GetUserNameEx
函数(如果第一个函数失败,则使用 GetUserName
函数)检查当前登录用户,并在用户不存在时将其添加到数据库。我假设使用此软件的计算机处于使用 Active Directory 身份验证的私有网络中,而大多数软件公司都是如此。
接下来,您应该通过“问题跟踪器”系统菜单中的设置...选项来选择您的设置。
现在您已准备好使用“问题跟踪器”应用程序了。
- 要添加新 bug,请单击“新建”,填写信息,然后单击“插入”;要中止操作,请按“取消”按钮。
- 要修改 bug 的数据,请从顶部的列表中选择它,单击“更改”,更新信息,然后单击“更新”;要中止操作,请按“取消”按钮。
- 要将 bug 的信息保存到 HTML 文档中,请从顶部的列表中选择它,单击“另存为...”,然后选择文件名,然后按“确定”。
- 要打印 bug 的信息,请从顶部的列表中选择它,单击“打印...”,然后选择您的打印机,然后按“确定”。
- 要按特定字段搜索数据库,请取消选择顶部的所有问题,填写搜索条件,然后按“搜索”。
实现
“问题跟踪器”应用程序的设计考虑了三个抽象层(因此可以轻松扩展此软件)。
- MFC/应用程序层 - 提供典型 Windows 应用程序的基本功能;此层在
CIssueTrackerApp
、CIssueTrackerDlg
、CUserConfigurationDlg
、CFileAttachementsDlg
、CConnectionSettingsDlg
和CSmtpConfigurationDlg
类中实现; - 对象/数据操作层 - 提供插入/删除/搜索用户、问题及其关联文件和注释的特定功能;它在
CIssueUserItem
、CIssueUserList
、CIssueFileItem
、CIssueFileList
、CIssueNoteItem
、CIssueNoteList
、CIssueDataItem
和CIssueDataListcode
类中实现; - ODBC 数据库访问层 - 提供通过 ODBC API 访问数据库的包装函数;它在
CDatabaseConnector
和CDatabaseRecordset
类中实现。
关于 MFC/应用程序层的类没什么好说的。
CIssueTrackerDlg
类是应用程序的主界面;CUserConfigurationDlg
类实现用户配置对话框;CFileAttachementsDlg
类允许用户对附加到 bug 的文件执行基本操作;CConnectionSettingsDlg
类允许用户设置数据库连接;CSmtpConfigurationDlg
类允许用户设置 SMTP 服务器连接。
CIssueUserItem
类是 IssueUser
表的包装器,它具有以下成员函数:
UINT GetID()
- 返回用户的 ID;SetID( UINT uintID )
- 设置用户的 ID;CString GetFullName()
- 返回用户的全名;void SetFullName( CString lpszFullName )
- 设置用户的全名;CString GetEmailAddr()
- 返回用户的电子邮件地址;void SetEmailAddr( CString lpszEmailAddr )
- 设置用户的电子邮件地址;GetNotification()
- 如果用户希望通过电子邮件收到通知,则返回TRUE
,否则返回FALSE
;void SetNotification( UINT uintNotification )
- 为用户启用/禁用电子邮件通知;void CopyDataFrom( CIssueUserItem * pIssueUser )
- 复制同类型另一个对象的数据。
CIssueUserList
类是 CIssueUserItem
对象的数组,它具有以下成员函数:
int GetSize()
- 返回当前可用用户的计数;CIssueUserItem * GetItemAt( int nItemIndex )
- 返回数组中nItemIndex
位置的用户;void SetItemAt( int nItemIndex, CIssueUserItem * pIssueUser )
- 将新用户设置到数组的nItemIndex
位置;void RemoveAllItems()
- 删除数组中存储的所有用户;BOOL LoadAllItems()
- 从数据库加载所有用户;BOOL InsertItem( CIssueUserItem * pIssueUser )
- 将一个新用户插入数组,也插入数据库;BOOL UpdateItem( CIssueUserItem * pIssueUser )
- 更新数组中用户的数据,也更新数据库;BOOL DeleteItem( CIssueUserItem * pIssueUser )
- 从数组中删除用户,也从数据库中删除;CIssueUserItem * SearchItem( UINT uintID, BOOL bUseDatabase )
- 根据用户的 ID 在数组/数据库中搜索用户;CIssueUserItem * SearchItem( CString lpszFullName, BOOL bUseDatabase )
- 根据用户的全名在数组/数据库中搜索用户;CIssueUserItem * SearchAddr( CString lpszEmailAddr, BOOL bUseDatabase )
- 根据用户的电子邮件地址在数组/数据库中搜索用户;UINT GetUserLoggedID()
- 返回当前登录用户的 ID;void SetUserLoggedID( UINT uintID )
- 设置当前登录用户的 ID;CString GetUserLoggedName()
- 返回当前登录用户的全名;void SetUserLoggedName( CString lpszName )
- 设置当前登录用户的全名;CString GetUserLoggedEmailAddr()
- 返回当前登录用户的电子邮件地址;void SetUserLoggedEmailAddr( CString lpszEmailAddr )
- 设置当前登录用户的电子邮件地址;BOOL ValidateCurrentUserLogged()
- 如果当前登录用户不在数据库中,则将其添加到数据库。
CIssueFileItem
类是 IssueFile
表的包装器,它具有以下成员函数:
UINT GetID()
- 返回文件的 ID;void SetID( UINT uintID )
- 设置文件的 ID;UINT GetIssueID()
- 返回与文件关联的问题的 ID;void SetIssueID( UINT uintIssueID )
- 设置与文件关联的问题的 ID;CIssueUserItem * GetAuthor()
- 返回文件的作者;void SetAuthor( CIssueUserItem * itemAuthor )
- 设置文件的作者;CString GetFileName()
- 返回文件名;void SetFileName( CString lpszFileName )
- 设置文件名;UINT GetFileSize()
- 返回文件大小(以字节为单位);void SetFileSize( UINT uintFileSize )
- 设置文件大小(以字节为单位);CTime GetModified()
- 返回文件的修改日期/时间;void SetModified( CTime timeModified )
- 设置文件的修改日期/时间;CLongBinary * GetContent()
- 获取文件内容;void SetContent( CLongBinary * byteContent )
- 设置文件内容;void CopyDataFrom( CIssueFileItem* pIssueFile )
- 复制同类型另一个对象的数据。
CIssueFileList
类是 CIssueFileItem
对象的数组,它具有以下成员函数:
int GetSize()
- 返回当前可用的文件数量(针对某个问题);CIssueFileItem * GetItemAt( int nItemIndex )
- 返回数组中nItemIndex
位置的文件;void SetItemAt( int nItemIndex, CIssueFileItem * pIssueFile )
- 将新文件设置到数组的nItemIndex
位置;void RemoveAllItems()
- 删除数组中存储的所有文件;BOOL LoadAllItems( UINT uintIssueID, CIssueUserList * listIssueUser )
- 从数据库加载与某个问题相关的所有文件;BOOL InsertItem( CIssueFileItem * pIssueFile )
- 将一个新文件插入数组,也插入数据库;BOOL UpdateItem( CIssueFileItem * pIssueFile )
- 更新数组中文件的数据,也更新数据库;BOOL DeleteItem( CIssueFileItem * pIssueFile )
- 从数组中删除文件,也从数据库中删除;CIssueFileItem * SearchItem( UINT uintID, CIssueUserList * listIssueUser, BOOL bUseDatabase )
- 根据文件的 ID 在数组/数据库中搜索文件。
CIssueNoteItem
类是 IssueNote
表的包装器,它具有以下成员函数:
UINT GetID()
- 返回注释的 ID;void SetID( UINT uintID )
- 设置注释的 ID;UINT GetIssueID()
- 返回与注释相关的问题的 ID;void SetIssueID( UINT uintIssueID )
- 设置与注释相关的问题的 ID;CIssueUserItem * GetAuthor()
- 返回注释的作者;void SetAuthor( CIssueUserItem * itemAuthor )
- 设置注释的作者;UINT GetFileSize()
- 返回注释的大小(以字节为单位);void SetFileSize( UINT uintFileSize )
- 设置注释的大小(以字节为单位);CTime GetModified()
- 返回注释的修改日期/时间;void SetModified( CTime timeModified )
- 设置注释的修改日期/时间;CLongBinary * GetContent()
- 获取注释的内容;void SetContent( CLongBinary * byteContent )
- 设置注释的内容;void CopyDataFrom( CIssueNoteItem* pIssueNote )
- 复制同类型另一个对象的数据。
CIssueNoteList
类是 CIssueNoteItem
对象的数组,它具有以下成员函数:
int GetSize()
- 返回当前可用的注释数量(针对某个问题);CIssueNoteItem * GetItemAt( int nItemIndex )
- 返回数组中nItemIndex
位置的注释;void SetItemAt( int nItemIndex, CIssueNoteItem * pIssueNote )
- 将新注释设置到数组的nItemIndex
位置;void RemoveAllItems()
- 删除数组中存储的所有注释;BOOL LoadAllItems( UINT uintIssueID, CIssueUserList * listIssueUser )
- 从数据库加载与某个问题相关的所有注释;BOOL InsertItem( CIssueNoteItem * pIssueNote )
- 将一个新注释插入数组,也插入数据库;BOOL UpdateItem( CIssueNoteItem * pIssueNote )
- 更新数组中注释的数据,也更新数据库;BOOL DeleteItem( CIssueNoteItem * pIssueNote )
- 从数组中删除注释,也从数据库中删除;CIssueNoteItem * SearchItem( UINT uintID, CIssueUserList * listIssueUser, BOOL bUseDatabase )
- 根据注释的 ID 在数组/数据库中搜索注释。
CIssueDataItem
类是 IssueData
表的包装器,它具有以下成员函数:
UINT GetID()
- 返回问题的 ID;void SetID( UINT uintID )
- 设置问题的 ID;CString GetTitle()
- 返回问题的标题;void SetTitle( CString lpszTitle )
- 设置问题的标题;CString GetProduct()
- 返回生成问题的产品;void SetProduct( CString lpszProduct )
- 设置生成问题的产品;CString GetComponent()
- 返回生成问题的组件;void SetComponent( CString lpszComponent )
- 设置生成问题的组件;CString GetVersion()
- 返回生成问题的软件版本;void SetVersion( CString lpszVersion )
- 设置生成问题的软件版本;CString GetPlatform()
- 返回生成问题的软件平台;void SetPlatform( CString lpszPlatform )
- 设置生成问题的软件平台;UINT GetStatus()
- 设置问题的状态;void SetStatus( UINT uintStatus )
- 返回问题的状态;UINT GetImportance()
- 返回问题的优先级;void SetImportance( UINT uintImportance )
- 设置问题的优先级;CString GetMilestone()
- 返回问题的截止日期;void SetMilestone( CString lpszMilestone )
- 设置问题的截止日期;CIssueUserItem * GetReporter()
- 返回报告此问题的用户;SetReporter( CIssueUserItem * itemReporter )
- 设置报告此问题的用户;CIssueUserItem * GetResolver()
- 返回应修复此问题的软件开发人员;void SetResolver( CIssueUserItem * itemResolver )
- 设置应修复此问题的软件开发人员;CIssueUserItem * GetConfirmer()
- 返回将确认此问题修复的 QA 联系人;void SetConfirmer( CIssueUserItem * itemConfirmer )
- 设置将确认此问题修复的 QA 联系人;CString GetDependencies()
- 返回此问题所依赖的其他问题的 ID;void SetDependencies( CString lpszDependencies )
- 设置此问题所依赖的其他问题的 ID;CString GetBlocksIssues()
- 返回此问题所阻止的其他问题的 ID;void SetBlocksIssues( CString lpszBlocksIssues )
- 设置此问题所阻止的其他问题的 ID;CString GetDistribution()
- 返回通知电子邮件的 CC 地址列表;void SetDistribution( CString lpszDistribution )
- 设置通知电子邮件的 CC 地址列表;CTime GetModified()
- 返回上次修改此问题的时间;void SetModified( CTime timeModified )
- 设置上次修改此问题的时间;void CopyDataFrom( CIssueDataItem* pIssueData )
- 复制同类型另一个对象的数据。
CIssueDataList
类是 CIssueDataItem
对象的数组,它具有以下成员函数:
int GetSize()
- 返回当前可用的问题数量;CIssueDataItem * GetItemAt( int nItemIndex )
- 返回数组中nItemIndex
位置的问题;void SetItemAt( int nItemIndex, CIssueDataItem * pIssueData )
- 将新问题设置到数组的nItemIndex
位置;void RemoveAllItems()
- 删除数组中存储的所有问题;BOOL LoadAllItems( BOOL bRefreshUsers, BOOL bLoadAllFiles, BOOL bLoadAllNotes, BOOL bFindRelations )
- 从数据库加载所有问题;BOOL InsertItem( CIssueDataItem * pIssueData )
- 将一个新问题插入数组,也插入数据库;BOOL UpdateItem( CIssueDataItem * pIssueData )
- 更新数组中问题的数据,也更新数据库;BOOL DeleteItem( CIssueDataItem * pIssueData )
- 从数组中删除问题,也从数据库中删除;CIssueNoteItem * SearchItem( UINT uintID, BOOL bUseDatabase )
- 根据问题的 ID 在数组/数据库中搜索问题;BOOL SearchEngine( CIssueDataItem * pIssueData, BOOL bRefreshUsers, BOOL bLoadAllFiles, BOOL bLoadAllNotes, BOOL bFindRelations )
- 搜索所有与pIssueData
中指定的字段匹配的问题;BOOL FindRelation( UINT uintID, CString &lpszBlocks )
- 搜索此问题所阻止的所有问题的 ID。
CDatabaseConnector
类是我们通过 ODBC 驱动程序访问数据库的基本资源,它具有以下成员函数:
void AllocConnectorHandle()
- 为数据库连接分配新的句柄;void FreeConnectorHandle()
- 释放之前为数据库连接分配的句柄;BOOL DriverConnect( LPCTSTR lpszInputConnection )
- 创建到数据库的新 ODBC 连接;BOOL OpenConnection()
- 打开到数据库的新 ODBC 连接;void CloseConnection()
- 关闭当前到数据库的 ODBC 连接;BOOL ExecuteStatement( LPCTSTR lpszStatement )
- 执行 SQL 语句(例如INSERT
、UPDATE
、DELETE
);void SetLoginTimeout( const long nLoginTimeout )
- 设置建立数据库连接的超时时间;long GetLoginTimeout()
- 返回建立数据库连接的超时时间;BOOL IsDatabaseOpened()
- 如果已建立数据库连接,则返回TRUE
,否则返回FALSE
;long GetRowsAffected()
- 返回上次执行的 SQL 查询所影响的行数;DATABASE_TYPE GetDatabaseType()
- 返回当前连接的数据库类型(例如 SQL SERVER、ORACLE、MYSQL 等);void SetDatabaseType( DATABASE_TYPE uintDatabaseType )
- 设置新连接的数据库类型。
CDatabaseRecordset
类利用 CDatabaseConnector
提供的连接,它具有以下成员函数:
void AllocRecordsetHandle()
- 为 SQL 语句分配新的句柄;void FreeRecordsetHandle()
- 释放之前为 SQL 语句分配的句柄;BOOL OpenRecordset( LPCTSTR lpszStatement )
- 执行 SQL 查询并获取数据行集;void CloseRecordset()
- 关闭由上一个 SQL 查询创建的数据行集;void GetFieldValue( WORD nFieldIndex, LONG nFieldType, DWORD nFieldSize, void* pDataBuffer )
- 从nFieldIndex
列获取指定类型nFieldType
的值,并将其存储到大小为nFieldSize
的pDataBuffer
中;void GetColumnAttr( WORD nColumnIndex, LPTSTR lpszColumnName, WORD nBufferLength, WORD &nColumnType, ULONG &nColumnSize, WORD &nColumnScale, BOOL &bNullable )
- 获取当前数据行集中某一列的属性;void SeekFirstRecord()
- 将光标移动到当前数据行集的第一条记录;void SeekLastRecord()
- 将光标移动到当前数据行集的最后一条记录;void SeekPrevRecord()
- 将光标移动到当前数据行集的前一条记录;void SeekNextRecord()
- 将光标移动到当前数据行集的下一条记录;long GetNumResultRows()
- 返回上次执行的 SQL 查询所影响的行数;long GetNumResultCols()
- 返回当前数据行集中的列数;void SetQueryTimeout( const long nQueryTimeout )
- 设置 SQL 查询的超时时间;long GetQueryTimeout()
- 返回为 SQL 查询选择的超时时间;BOOL IsRecordsetOpened()
- 如果数据行集可用,则返回TRUE
,否则返回FALSE
;BOOL IsRecordsetBOF()
- 如果光标已到达数据行集的开头,则返回TRUE
,否则返回FALSE
;BOOL IsRecordsetEOF()
- 如果光标已到达数据行集的末尾,则返回TRUE
,否则返回FALSE
。
关注点
众所周知,Windows 提供了对注册表数据库的便捷访问。因此,“问题跟踪器”应用程序很好地利用了它来存储数据库访问设置。但我无法将访问密码以纯文本格式存储。所以我查看了 Microsoft Cryptography Library,并决定为此目的实现两个辅助函数:GetRegistryPassword
和 SetRegistryPassword
。
/// Returns a decrypted password read from Windows Registry, using Crypto API calls
BOOL GetRegistryPassword( LPCTSTR lpszCryptoKey, LPCTSTR lpszSection, LPCTSTR lpszEntry, LPTSTR lpszValue, LPCTSTR lpszDefault )
{
if ( !lpszSection || !lpszEntry || !lpszValue )
return FALSE;
if ( !USE_CRYPTO_METHODS )
{
return ( _tcscpy( lpszValue, AfxGetApp()->GetProfileString( lpszSection, lpszEntry, lpszDefault ) ) != NULL);
}
if ( !lpszCryptoKey )
lpszCryptoKey = AfxGetAppName();
LPBYTE lpcbPassword = (LPBYTE) lpszCryptoKey;
const DWORD dwPasswordLen = (DWORD) ( sizeof(TCHAR) * _tcslen( lpszCryptoKey ) );
BYTE lpcbDataValue[PASSWORD_MAXLENGTH];
DWORD dwHowManyBytes = 0;
LPBYTE lpcbTempBuffer = NULL;
BOOL bDecryptionDone = FALSE;
HCRYPTPROV hCryptoProvider = NULL;
HCRYPTHASH hCryptoHash = NULL;
HCRYPTKEY hCryptoKey = NULL;
if ( AfxGetApp()->GetProfileBinary( lpszSection, lpszEntry, (LPBYTE *)&lpcbTempBuffer, (UINT *)&dwHowManyBytes ) )
{
if ( dwHowManyBytes != 0)
{
ZeroMemory( lpcbDataValue, sizeof( lpcbDataValue ) );
CopyMemory( lpcbDataValue, lpcbTempBuffer, dwHowManyBytes );
if ( CryptAcquireContext( &hCryptoProvider, NULL, NULL, PROV_RSA_FULL, 0 ) )
{
if ( CryptCreateHash( hCryptoProvider, CALG_MD5, NULL, 0, &hCryptoHash ) )
{
if ( CryptHashData( hCryptoHash, lpcbPassword, dwPasswordLen, 0 ) )
{
if ( CryptDeriveKey( hCryptoProvider, CALG_RC4, hCryptoHash, CRYPT_EXPORTABLE, &hCryptoKey ) )
{
if ( CryptDecrypt( hCryptoKey, NULL, TRUE, 0, lpcbDataValue, &dwHowManyBytes ) )
{
bDecryptionDone = TRUE;
_tcscpy( lpszValue, (LPTSTR) lpcbDataValue );
}
else
{
DisplayLastError( _T("CryptDecrypt: ") );
}
VERIFY( CryptDestroyKey( hCryptoKey ) );
}
else
{
DisplayLastError( _T("CryptDeriveKey: ") );
}
}
else
{
DisplayLastError( _T("CryptHashData: ") );
}
VERIFY( CryptDestroyHash( hCryptoHash ) );
}
else
{
DisplayLastError( _T("CryptCreateHash: ") );
}
VERIFY( CryptReleaseContext( hCryptoProvider, 0 ) );
}
else
{
DisplayLastError( _T("CryptAcquireContext: ") );
}
}
}
else
{
if ( !dwHowManyBytes )
{
_tcscpy( lpszValue, lpszDefault );
bDecryptionDone = TRUE;
}
}
if ( lpcbTempBuffer != NULL )
delete lpcbTempBuffer;
return bDecryptionDone;
}
和
/// Writes an encrypted password to Windows Registry, using Crypto API calls
BOOL SetRegistryPassword( LPCTSTR lpszCryptoKey, LPCTSTR lpszSection, LPCTSTR lpszEntry, LPTSTR lpszValue )
{
if ( !lpszSection || !lpszEntry || !lpszValue )
return FALSE;
if ( !USE_CRYPTO_METHODS )
{
return AfxGetApp()->WriteProfileString( lpszSection, lpszEntry, lpszValue );
}
if ( !lpszCryptoKey )
lpszCryptoKey = AfxGetAppName();
LPBYTE lpcbPassword = (LPBYTE) lpszCryptoKey;
const DWORD dwPasswordLen = (DWORD)( sizeof( TCHAR ) * _tcslen( lpszCryptoKey ) );
BYTE lpcbDataValue[PASSWORD_MAXLENGTH];
const DWORD dwDataValueLen = PASSWORD_MAXLENGTH;
DWORD dwHowManyBytes = dwDataValueLen;
BOOL bEncryptionDone = FALSE;
HCRYPTPROV hCryptoProvider = NULL;
HCRYPTHASH hCryptoHash = NULL;
HCRYPTKEY hCryptoKey = NULL;
ZeroMemory( lpcbDataValue, sizeof( lpcbDataValue ) );
CopyMemory( lpcbDataValue, lpszValue, dwDataValueLen );
if ( CryptAcquireContext( &hCryptoProvider, NULL, NULL, PROV_RSA_FULL, 0 ) )
{
if ( CryptCreateHash( hCryptoProvider, CALG_MD5, NULL, 0, &hCryptoHash ) )
{
if ( CryptHashData( hCryptoHash, lpcbPassword, dwPasswordLen, 0 ) )
{
if ( CryptDeriveKey( hCryptoProvider, CALG_RC4, hCryptoHash, CRYPT_EXPORTABLE, &hCryptoKey ) )
{
if ( CryptEncrypt( hCryptoKey, NULL, TRUE, 0, lpcbDataValue, &dwHowManyBytes, dwDataValueLen ) )
{
bEncryptionDone = AfxGetApp()->WriteProfileBinary( lpszSection, lpszEntry, lpcbDataValue, (UINT)dwHowManyBytes );
}
else
{
DisplayLastError( _T("CryptEncrypt: ") );
}
VERIFY( CryptDestroyKey( hCryptoKey ) );
}
else
{
DisplayLastError( _T("CryptDeriveKey: ") );
}
}
else
{
DisplayLastError( _T("CryptHashData: ") );
}
VERIFY( CryptDestroyHash( hCryptoHash ) );
}
else
{
DisplayLastError( _T("CryptCreateHash: ") );
}
VERIFY( CryptReleaseContext( hCryptoProvider, 0 ) );
}
else
{
DisplayLastError( _T("CryptAcquireContext: ") );
}
return bEncryptionDone;
}
其他敏感数据(仅存储在数据库中)使用 MySQL 的 AES_ENCRYPT
和 AES_DECRYPT
函数进行加密。您可以在此处找到有关此主题的更多信息。
如果您想在 MS SQL Server 或 Oracle 数据库上运行此应用程序,则需要修改 IssueTrackerExt.cpp 文件中的 SQL 语句。
最后的寄语
“问题跟踪器”应用程序使用了许多在 The Code Project 上发布的组件。非常感谢:
- PJ Naughter 的 CPJNSMTPConnection 类
- Ben Hanson 的 CFilterEdit 类
- Dominik Reichl 的 CSecureEdit 类
- Paul DiLascia 的 CModuleVersion 类
- Tim McColl 的 CReadOnlyComboBox 类
- Dan Madden 的 CTokenEx 类
为了重新编译此 Visual C++ .NET 2005 项目,您应该考虑从 Boost 官方页面下载 boost::regex
库,因为我正在使用它来读取电子邮件地址和问题的依赖项列表。
PJ Naughter 的 CPJNSMTPConnection
类还需要 OpenSSL Project。一个不错的选择是从 Shining Light Productions 页面下载 OpenSSL Project 的 Windows 二进制文件。
虽然此工具最初仅供我个人使用,但现在它是一个“社区”项目,如果您觉得它有用并希望提出增强功能或 bug 修复的建议,请在下方发帖。
历史
- 版本 1.00(2014 年 7 月 13 日)- 初始发布。