MFCで在庫管理システム作成 2

前回までの記事。

ユーザ情報・カテゴリー

まずは、ユーザに関する情報
ユーザID、ユーザ名、ユーザ権限の変数とゲットセット関数を作ります。

ついでに、今回カテゴリーはシステム内に保持させるので、
それも作成。

InventoryManagementSystemというプロジェクトを作ったら、
InventoryManagementSystem.hがあるのでそこに定義。
(InventoryManagementSystemDlg.hではないです)

class CInventoryManagementSystemApp : public CWinApp
{
// 略

private:
	CString m_strUserID;	// ユーザID
	CString m_strUserName;	// ユーザ名
	BOOL m_bUserAdmin;		// ユーザ権限 TRUE=管理者、FALSE=一般
	const int m_iCategoryCnt = 3;	// カテゴリー数
	const CString m_strArrayCategory[3] = {_T("Food), _T("Stationery"), _T("Electronic equipment")};	// カテゴリー配列

public:
	// ユーザID設定
	void SetUserID(CString strUserID) { m_strUserID = strUserID; }
	// ユーザID取得
	CString strGetUserID() { return m_strUserID; }

	// ユーザ名設定
	void SetUserName(CString strUserName) { m_strUserName = strUserName; }
	// ユーザ名取得
	CString strGetUserName() { return m_strUserName; }

	// ユーザ権限設定
	void SetUserAdmin(BOOL bUserAdmin) { m_bUserAdmin = bUserAdmin; }
	// ユーザ権限取得
	BOOL bGetUserAdmin() { return m_bUserAdmin; }

	// カテゴリー数返却
	const int iGetCategoryCnt() { return m_iCategoryCnt; }
	// カテゴリー返却
	const CString strGetCategory(int iId) { return m_strArrayCategory[iId]; }
};

変数はprivateで、設定取得はpublicで。

カテゴリー関連は更新する必要ないので、
ゲット関数だけです。

最後に、InventoryManagementSystem.cppへ、
ユーザ情報の初期化を記述します。

CInventoryManagementSystemApp::CInventoryManagementSystemApp()
{
	// 略

	m_strUserID = _T("");	// ユーザID
	m_strUserName = _T("");	// ユーザ名
	m_bUserAdmin = FALSE;	// ユーザ権限
}

一度ビルドして、ビルドエラーが出てこないか確認。

ログイン画面作成

メインよりも先にこっち作るのもどうか、ですが。

簡単なのでこっちを終わらせます。

自分で色々システム作って、そのたびにログイン画面作ってるので、
もう自分の中でログイン画面作成手順書が出来上がってるんですよね(笑)。

まずは新規ダイアログ。

リソースビューからコンプロジェクトのDialogフォルダを右クリックし、
「リソースの追加」。

新規作成します。

プロパティからダイアログIDを変更。

初期値はIDD_DIALOG1になってる(はずな)ので、
IDD_DIALOG_LOGINにでも変更しておきます。

ここに、スタティックテキスト2つ、エディットボックス2つ用意。
OKボタンはキャプションを変えて「ログイン」にでも。

レイアウトはお好みで…。

( ˘ω˘ )サクセイチュウ

自分はいつも、これ位が気に入ってます。

パスワードエディットボックスは「パスワード」をTrueに、
ユーザIDエディットボックスは、「数字」をTrueにします。

各IDやコントロール位置は、このようになってます。

IDD_DIALOG_LOGIN DIALOGEX 0, 0, 166, 134
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "ログイン",IDOK,20,100,50,14
    PUSHBUTTON      "キャンセル",IDCANCEL,90,100,50,14
    LTEXT           "ユーザID",IDC_STATIC_LOGIN_ID,20,20,30,8
    EDITTEXT        IDC_EDIT_LOGIN_ID,20,30,100,14,ES_AUTOHSCROLL | ES_NUMBER
    LTEXT           "パスワード",IDC_STATIC_LOGIN_PASSWORD,20,50,29,8
    EDITTEXT        IDC_EDIT_LOGIN_PASSWORD,20,60,100,14,ES_PASSWORD | ES_AUTOHSCROLL
END

IDの付け方は、基本的に
IDC_コントロール名_画面_何の項目
と自分の中でルールづけてます。

クラス作成

さっきのリソースビューの画面から適当に、
何でもいいのでコントロールをダブルクリック。
ログインボタンでいいですかね。

するとクラス作成画面が出てくるので、
クラス名・h名・cpp名を決定します。

クラス名はCLogin、h名はLogin.h、cpp名はLogin.cppに命名。
これでOK。

ちゃんと2種、作成されました。

続いて、表示メニューから「クラスビュー」なるものを開きます。

開いて、ログインクラスのCLoginを選択。

そのままプロパティ画面を開き、
「オーバーライド」を選択。
色々な関数が出てきます。

OnInitDialogを追加します。

最後に、再びリソースビューから画面を開き、
OK(ログイン)ボタンとキャンセルボタンをダブルクリック。
自動で関数が追加されます。

こんな感じに3つ、関数が追加されます。

これで、CLoginクラスをいじる準備ができました。

具体的な機能は後回しで、先に作っておきたいモノを作ります。

ユーザID、パスワード変数

Login.hを開き、色々作成します。

class CLogin : public CDialogEx
{
	// 略

private:
	CString m_strLoginUserID;		// ユーザID
	CString m_strLoginPassword;		// ログインパスワード
};

エディットボックスに入れられた文字列を
CStringの変数に格納させます。

コンストラクタ

ここから先は、Login.cppで処理を書きます。

メンバ変数を作ったので、初期化を忘れずに。

CLogin::CLogin(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_DIALOG_LOGIN, pParent)
{
	m_strLoginUserID = _T("");		// ログインユーザID
	m_strLoginPassword = _T("");	// ログインパスワード
}

DoDataExchange

CString型変数とコントロールIDを紐づけます。

void CLogin::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Text(pDX, IDC_EDIT_LOGIN_ID, m_strLoginUserID);					// ユーザID
	DDX_Text(pDX, IDC_EDIT_LOGIN_PASSWORD, m_strLoginPassword);			// パスワード
}

キャンセルボタン押下時

正直、これ必要なのかなぁって思う面は多々あるのですが、
念のためいつも作ってます。

キャンセルボタンが押されたときに呼ばれる関数を修正。

/**
 * @fn
 * @brief	キャンセルボタン押下時の処理
 * @param	なし
 * @return	なし
 */
void CLogin::OnBnClickedCancel()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	CDialogEx::OnCancel();
	PostQuitMessage(0); // アプリケーション終了
}

11行目追加しただけなんですけどね。

OnInitDialog

今のところ作りたい機能は画面キャプションだけです。

/**
 * @fn
 * @brief	ダイアログ初期処理
 * @param	なし
 * @return	TRUE	成功
 *			FALSE	失敗
 */
BOOL CLogin::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// TODO: ここに初期化を追加してください

	SetWindowText(_T("ログイン画面"));

	return TRUE;  // return TRUE unless you set the focus to a control
	// 例外 : OCX プロパティ ページは必ず FALSE を返します。
}

ログインダイアログ出現

機能とかひとまずいいので、
こいつをメイン画面表示前に出現させます。

再び、InventoryManagementSystem.cppへ。

ログインヘッダーをインクルードさせます。

#include "pch.h"
#include "framework.h"
#include "InventoryManagementSystem.h"
#include "InventoryManagementSystemDlg.h"
#include "Login.h"	// ログイン画面

5行目を追加です。

続いて、InitInstance関数を改良。

CInventoryManagementSystemDlgのコンストラクタを作成している箇所があるので、
その直前にログイン画面を出します。

BOOL CInventoryManagementSystemApp::InitInstance()
{
	// 略

	CLogin dlgLogin;
	if (dlgLogin.DoModal() != IDOK)
	{
		// キャンセルが押された場合、アプリケーションを終了
		return FALSE;
	}

	CInventoryManagementSystemDlg dlg;
	m_pMainWnd = &dlg;
	INT_PTR nResponse = dlg.DoModal();
	// 略

	// ダイアログは閉じられました。アプリケーションのメッセージ ポンプを開始しないで
	//  アプリケーションを終了するために FALSE を返してください。
	return FALSE;
}

これで、一番最初にログイン画面が出され、
OK(ログイン)ボタンが押されるとメイン画面へ、
キャンセルを押されるとダイアログが閉じるように。

いったんビルドしましょう。

動作確認

exeを起動させると。

予期通りのダイアログです。

エディットボックスには適当に文字を入れることができます。
(IDは数字のみ)

ログインを押すと。

メイン画面の登場です。

キャンセル押すと、そのまま閉じます。

MySQL操作クラス作成

自分ルールですが、
MySQLの操作は別クラスでひとまとめにします。
クラス名はCMySQLManager、h名はMySQLManager.h、cppはMySQLManager.cpp。

この中で、テーブルを覗いたり更新したりします。

MySQLManager.h作成

最初なので、画像付きで。

ヘッダーフォルダを右クリックし、
追加→新しい項目
を選択します。

ヘッダーを選択し、
MySQLManager.hと名前を付け、追加ボタンを押します。

何もないファイルの出来上がり。

ここに、一気に処理を定義します。
後々、色々なところで使う、いわゆる共通処理です。
ちょっと長いですが…。

ついでに、ログイン用の関数も作成。

#pragma once
#include <mysql/jdbc.h>
#include <string>

class CMySQLManager
{
	// 変数
private:
	// DB接続関連
	const sql::SQLString HOST = "tcp://127.0.0.1:3306";
	const sql::SQLString USER = "root";
	const sql::SQLString PASSWORD = "〇〇〇"; //←ここは自身のパスワード

	// スキーマ
	const sql::SQLString SCHEMA = "inventory_management";
	// テーブル
	const sql::SQLString TABLE_USER = "user";		// ユーザテーブル
	const sql::SQLString TABLE_PRODUCT = "product";	// 商品テーブル
	const sql::SQLString TABLE_LOGS = "logs";		// 履歴テーブル

	// 列名
	// ユーザテーブル
	const sql::SQLString COL_USER_ID = "user_id";
	const sql::SQLString COL_USER_NAME = "user_name";
	const sql::SQLString COL_USER_PASSWORD = "password";
	const sql::SQLString COL_USER_ADMIN = "admin";

	// 商品テーブル
	const sql::SQLString COL_PRODUCT_ID = "product_id";
	const sql::SQLString COL_PRODUCT_NAME = "product_name";
	const sql::SQLString COL_PRODUCT_CATEGORY = "category";
	const sql::SQLString COL_PRODUCT_UP = "unit_price";
	const sql::SQLString COL_PRODUCT_COUNT = "product_count";
	const sql::SQLString COL_PRODUCT_ALERT = "product_alert";
	const sql::SQLString COL_PRODUCT_DISCONTINUE = "discontinue";

	// 履歴テーブル
	const sql::SQLString COL_LOGS_ID = "log_id";
	const sql::SQLString COL_LOGS_OPERATION = "operation";
	const sql::SQLString COL_LOGS_PRODUCT_ID = "product_id";
	const sql::SQLString COL_LOGS_TIME_STAMP = "time_stamp";

	// 接続状況
	std::shared_ptr<sql::Connection> m_con;


	// 関数
private:
	// エラーひとまとめ
	void ErrSQLException(sql::SQLException& e);
	// CString→SQLString変換処理
	sql::SQLString strConverter(CString str);
	// SQLString→CString変換処理
	CString strConvertFromUTF8ToUTF16(sql::SQLString strCol);
	//ハッシュ化
	sql::SQLString strHashing(CString strPassword);

public:
	// コンストラクタ
	CMySQLManager();

	//デストラクタ
	~CMySQLManager();

	//接続
	std::shared_ptr<sql::Connection> GetConnection();

	//ログインチェック
	BOOL bCheckLogin(CString strId, CString strPassword);
};

ちょっと解説。

  • 10~12行目:MySQLへ接続するための情報です。
    パスワードはご自身のものを。
  • 15~41行目:スキーマやテーブル名です。
    列名まで定義するのはやりすぎかもしれませんが、
    今回はこれを使用します。
  • 44行目:接続用の変数(としか自分の中では言えない)
  • 50~56行目:エラー出力関数、型変換関数、
    後はパスワードをハッシュ化させる関数です。
  • 65行目:接続確立用関数
  • 69行目:ログイン用の関数

なお、この時点ではエラーが出ると思います。
(赤波線がたくさん。主にCString。)

次で解消するのでご安心を。

MySQLManager.cpp作成

同じ要領で、ソースフォルダを右クリックし、
追加→新しい項目
を選択します。

今度はC++ファイルを選択し、
MySQLManager.cppと名前を付け、追加ボタンを押します。

出来上がったcppに、次の3つをinclude宣言。

#include "pch.h"
#include "InventoryManagementSystem.h"	//ここに当プロジェクトのヘッダー
#include "MySQLManager.h"

これで、ヘッダーファイルのエラーは解消されるはずです。

ひとまず、ログインチェック以外を記述します。
これまた長いですが。

#include "pch.h"
#include "InventoryManagementSystem.h"	//ここに当PJのヘッダー
#include "MySQLManager.h"

/**
 * @fn
 * @brief	コンストラクタ
 * @param	なし
 * @return	なし
 */
CMySQLManager::CMySQLManager()
{

}

/**
 * @fn
 * @brief	デストラクタ
 * @param	なし
 * @return	なし
 */
CMySQLManager::~CMySQLManager()
{
    // デストラクタで接続をクローズ
    if (m_con && m_con->isClosed() == false)
    {
        m_con->close();
    }
}

/**
 * @fn
 * @brief	エラー情報出力関数
 * @param	SQLException    SQLの例外
 * @return	なし
 */
void CMySQLManager::ErrSQLException(sql::SQLException& e)
{
    //接続できなかったため、エラーを表示させる
    CString errorMessage = _T("エラー:") + CString(e.what());
    CString errMsgtemp;
    errMsgtemp.Format(_T("\r\nエラーコード: %d"), e.getErrorCode());
    errorMessage += errMsgtemp;
    AfxMessageBox(errorMessage, MB_OK | MB_ICONERROR);
}

/**
 * @fn
 * @brief	CString→SQLString変換関数
 * @param	CString     変換元文字列
 * @return	SQLString   変換後文字列
 */
sql::SQLString CMySQLManager::strConverter(CString str)
{
    CT2CA pszConvertedAnsiString(str);
    std::string strStd(pszConvertedAnsiString);
    sql::SQLString sqlStrUserID(strStd);

    return sqlStrUserID;
}

/**
 * @fn
 * @brief	SQLString→CString変換関数
 * @param	SQLString   変換元文字列
 * @return	CString     変換後文字列
 */
CString CMySQLManager::strConvertFromUTF8ToUTF16(sql::SQLString strCol)
{
    CString strRtn = _T("");
    int len = MultiByteToWideChar(CP_UTF8, 0, strCol.c_str(), -1, NULL, 0);
    if (len > 0)
    {
        std::wstring wideName(len, 0);
        MultiByteToWideChar(CP_UTF8, 0, strCol.c_str(), -1, &wideName[0], len);
        // std::wstringからCStringに変換
        CString cstrCol(wideName.c_str());
        strRtn = cstrCol;
    }

    return strRtn;
}

/**
 * @fn
 * @brief	パスワードハッシュ化関数
 * @param	CString     生パスワード
 * @return	SQLString   ハッシュ化済文字列
 */
sql::SQLString CMySQLManager::strHashing(CString strPassword)
{
    CT2CA pszConvertedAnsiString(strPassword);
    std::string strStd(pszConvertedAnsiString);
    std::hash<std::string> hash_fn;

    std::string strValue = std::to_string(hash_fn(strStd)); //ハッシュ化処理
    return sql::SQLString(strValue);    //SQLString型に変更
}

/**
 * @fn
 * @brief	接続確立関数
 * @param	なし
 * @return	std::shared_ptr<sql::Connection>    コネクション(?)
 */
std::shared_ptr<sql::Connection> CMySQLManager::GetConnection()
{
    if (!m_con || m_con->isClosed())
    {
        try
        {
            sql::mysql::MySQL_Driver* driver = sql::mysql::get_mysql_driver_instance();

            // 新しい接続を作成
            m_con = std::shared_ptr<sql::Connection>(driver->connect(HOST, USER, PASSWORD));
        }
        catch (sql::SQLException& e)
        {
            // 接続でエラーが生じた
            ErrSQLException(e);
        }
    }

    return m_con;
}

そんなに大したことはしてないです。
もし詳細を知りたければ、ご自身でお調べ願います。

GetConnection()の使い方は後述します。

さぁ、ログインチェック関数を作ります。

GetConnection()の真下に、作成。

/**
 * @fn
 * @brief	ログインチェック
 * @param	CString         ユーザID
 * @param   CString         パスワード
 * @return	BOOL    TRUE    ログイン成功
 *                  FALSE   ログイン失敗
 */
BOOL CMySQLManager::bCheckLogin(CString strId, CString strPassword)
{
    try
    {
        // スキーマセット
        m_con->setSchema(SCHEMA);

        sql::SQLString sqlStrId = strConverter(strId);          // ユーザID    SQLString化
        sql::SQLString sqlStrPass = strHashing(strPassword);    // パスワード  ハッシュ化

        sql::SQLString strSqlQuery = "SELECT * FROM " + TABLE_USER + " WHERE " + COL_USER_ID + " = '" + sqlStrId + "' AND " + COL_USER_PASSWORD + " = '" + sqlStrPass + "'";
        // SELECT * FROM user WHERE user_id = '00000' AND password = 'ハッシュ化文字列'

        //クエリ実行
        std::unique_ptr<sql::PreparedStatement> pstmt(m_con->prepareStatement(strSqlQuery));

        std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

        CString strUserName = _T("");
        BOOL bUserAuth = FALSE;
        if (!res->next())
        {
            AfxMessageBox(_T("Idまたはパスワードが見当たらない"), MB_ICONERROR);
            return FALSE;
        }
        else
        {
            // IDとパスワードが見つかった!!

            //ユーザ名取得
            std::string strCol = res->getString(COL_USER_NAME);
            strUserName = strConvertFromUTF8ToUTF16(strCol);
            //ユーザ権限取得
            int iAuth = res->getInt(COL_USER_ADMIN);
            if (iAuth == 1)
            {
                bUserAuth = TRUE;
            }

            //ログイン情報保持
            CInventoryManagementSystemApp* pApp = static_cast<CInventoryManagementSystemApp*>(AfxGetApp());
            pApp->SetUserID(strId);         // ユーザID
            pApp->SetUserName(strUserName); // ユーザ名
            pApp->SetUserAdmin(bUserAuth);  // ユーザ権限
        }
    }
    catch (sql::SQLException& e)
    {
        ErrSQLException(e);
        return FALSE;
    }

    return TRUE;
}

頑張って少し解説(訳:仕事終わりで疲れてます…)

  • 14行目:使うスキーマセット
  • 16、17行目:SQLString化。パスワードはハッシュ化
  • 19行目:SQL文作成
  • 23~25行目:実行
  • 29~33行目:合致する行がなかった→関数を抜ける
  • 36行目~:特定の行を見るけることができた!
    (IDはテーブルの主キーなので、2行以上取得することはありません)
  • 39~46行目:ユーザ名とユーザ権限を取得
  • 49~52行目:システム内にユーザ情報を保持させます。

後はこれを呼応します…。

ログイン画面 ログインチェック実装

Login.cppに戻ります。

このcppを触るのはこれで最後ですかね。

まずは、MySQLManager.hをinclude。
Login.hをincludeしている真下に。

#include "MySQLManager.h"

続いて、事前に作った、
OK(ログイン)ボタンが押された時に呼ばれる関数OnBnClickedOkに処理を書きます。

/**
 * @fn
 * @brief	ログインボタン押下時の処理
 * @param	なし
 * @return	なし
 */
void CLogin::OnBnClickedOk()
{
	//メンバ変数に、エディットボックスの文字列を入れる
	UpdateData(TRUE);

	if (m_strLoginUserID == _T("") || m_strLoginPassword == _T(""))
	{
		// ログインIDまたはパスワードが埋まっていない
		AfxMessageBox(_T("IDまたはパスワードを入力してください。"), MB_ICONERROR);
		return;
	}

	//両方埋まっている
	CMySQLManager Manager;		// コンストラクタ
	Manager.GetConnection();	// 接続確立

	if (Manager.bCheckLogin(m_strLoginUserID, m_strLoginPassword) == TRUE)
	{
		// ログイン成功!!
		CDialog::OnOK();
	}
	else
	{
		//何らかの形でログイン失敗
		return;
	}
}

少々解説

  • 10行目:メンバ変数を更新させます。
  • 12~17行目:エディットボックスのどちらかが空の場合、
    処理を続行させません。
  • 21行目:接続を確立させます。
  • 23行目:さっき作った関数へ飛び、ログインできるかの判断。

以上ができたら、さぁビルド。

動作確認2

色々試します。

1.どちらかに何も入力しない場合

入力しろメッセージが出てくれました。
(メッセージボックスはわかりやすくするため、位置をずらしてます。)
ダイアログは閉じません。

2.間違ったパスワードを入れる

今回、正しい組み合わせは
ID:00000⇒パスワード:12345
ID:12345⇒パスワード:aaa

です。違うものを入れると。

そんなユーザいねぇよ、って怒られます。
メッセージ内容、もう少し改良した方がいいですかね。

3.正しいIDとパスワードを入力

本命、正しいものを入れると…。

メイン画面に移ってくれました。

よし( ´∀`)b

4.おまけ

あんまりやりたくないおまけですが。
ErrSQLException関数へ飛ぶ具体例を。

当然、ヘッダーファイル記載の
スキーマ名・テーブル名・列名などが間違っているとErrSQLExceptionに入ります。
興味あれば(?)試してみてください。

それとは別の例です。

今、Relaseモードでビルドを行っています。
これをDebugモードで作成します。

Debugモードに変更した際、
再度プロパティでインクルードディレクトリの追加等の3タスクを行ってください。

ビルドしてログインを実行すると…。

はい、ErrSQLExceptionに入りました。
エラーは、GetConnection関数の、

m_con = std::shared_ptr<sql::Connection>(driver->connect(HOST, USER, PASSWORD));

でcatchに入ります。

(…前、別のところだった気もするけど…)

未だに、なんでDebugモードだと接続できないのかわからないんですよね。
Releaseモードだと、色々上手いこと解釈してくれる、というのは知ってるんですが、
これが該当するのか…。

なので、SQLを使うシステムを作る際は必ずReleaseモードにしています。

用は済んだので、
Releaseモードに戻すことを忘れずに

終わりに

ログイン画面を作りました。

入力チェックは未入力だけにしてますが、
設計自体で色々付け加えられますね。
(IDが5桁じゃない、とか)

SQL操作の共通処理も作ったので、今後どんどん使用していきます。

しかしまぁあれだ。

書き始めると止まらずにどんどん進めちゃうな。

リハビリなんだから、もう少し書く量減らしてもいいのに…。
このペースだと力尽きる未来が。

次回はメイン画面の枠と、編集系ですかな。

MFCで在庫管理システム作成 3
前回までの記事。メイン画面 コントロール配置さて、メイン画面に必要なコントロールを置いていきます。今回必要なのは、・リストコントロール・カテゴリー用のコンボボックス・編集ボタン・履歴ボタン・コンボボックス用チェックボックス・ログイン情報用ス...

今回はここまで。

コメント

タイトルとURLをコピーしました