MFCで物品購入承認システム作成 7

前回までの話。

MFCで物品購入承認システム作成 1

MFCで物品購入承認システム作成 2

MFCで物品購入承認システム作成 3

MFCで物品購入承認システム作成 4

MFCで物品購入承認システム作成 5

MFCで物品購入承認システム作成 6

グループテーブル・項目テーブルからデータ取得

今回は購入用の項目をテーブルから取得する処理を作ります。

取得するタイミングですが、
ログインが成功した直後にします。
処理で言うとメイン画面を表示させるOnInitDialog内ですね。

ここで取得する理由は、
一般ユーザの場合、項目なんざ一回取得してしまえば
何度もDBのテーブルを見に行く必要はありません
処理で、ハッシュテーブルにでも格納する予定です。

管理者ユーザの場合は、
項目を変更したら再度ハッシュテーブルを更新すればいいだけです。

「管理者ユーザと一般ユーザが(別端末で)同時にログインしていて、
 管理者が項目いじったらログイン中の一般ユーザにも反映させなければいけないのでは」
などのアレはありますが、今回はそんなシチュエーションは考えません。

管理者よ、
一般ユーザの勤務時間外に項目はいじりなさい。

まず、SupplyRequestManager.hを開きます。
SupplyRequestManagerDlg.hではないです。

そこに、二つインクルード文を追加します。

#include <unordered_map>
#include <string>

マップを使う準備です。
見ればわかる通り、「unordered」なので順は保証されません。

続いて構造体を3つ定義。

struct KoumokuValue
{
	int iGroupID;
	CString strKoumokuName;
	CString strKoumokuPrice;
};

// CString用のカスタムハッシュ関数
struct CStringHash
{
	std::size_t operator()(const CString& str) const
	{
		return std::hash<std::wstring>{}(std::wstring(str));  // CStringをstd::wstringに変換してハッシュを計算
	}
};

// CString用の等値比較関数
struct CStringEqual
{
	bool operator()(const CString& str1, const CString& str2) const
	{
		return str1.Compare(str2) == 0;  // CStringの等値比較
	}
};

class CSupplyRequestManagerApp : public CWinApp
{
//略

最初のKoumokuValueは項目の構造体です。
後の2つですが、これは後で説明を。

今度はSupplyRequestManagerDlg.hを開きます。
グループ用と項目用でマップを2つ用意。

private:
	std::unordered_map<int, CString> m_MapGroup;		// グループテーブル格納
	std::unordered_map<int, KoumokuValue> m_MapKoumoku;	// 項目テーブル格納
public:
	std::unordered_map<int, CString>* GetMapGroup() { return &m_MapGroup; }
	std::unordered_map<int, KoumokuValue>* GetMapKoumoku() { return &m_MapKoumoku; }

OnInitDialogで処理を書きます。
SupplyRequestManagerDlg.cppを開き、ヘッダーをインクルード。

#include "MySQLManager.h"

OnInitDialogを見ると、
管理者ユーザか一般ユーザでボタンの表示を切り替える処理を作っているはずなので、
その下らへんで処理を追加。

BOOL CSupplyRequestManagerDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 略

	CMySQLManager GroupKoumokuGetter;
	GroupKoumokuGetter.GetConnection();	//接続確立

	GroupKoumokuGetter.SetGroupMap(GetMapGroup());		// グループテーブルデータをマップに格納
	GroupKoumokuGetter.SetKoumokuMap(GetMapKoumoku());	// 項目テーブルデータをマップに格納

	return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

例によって、まだ10と11行目の関数は作成していないのでエラーになりますが。
これで、メンバ変数のマップにテーブルのデータが格納されます。
それを、申請画面や項目リスト画面で活用します。

さて、SQL側の処理。

MySQLManager.hに先ほどの関数を定義。

public:
	//グループマップセット
	void SetGroupMap(std::unordered_map<int, CString>* MapGroup);
	//項目マップセット
	void SetKoumokuMap(std::unordered_map<int, KoumokuValue>* MapKoumoku);

MySQLManager.cppに移動し、処理を記述します。
やってることはそんなに難しいわけではない…はずです。

まずはグループテーブルの方

/**
 * @fn
 * @brief	グループテーブルセット
 * @param	std::unordered_map<int, CString>* マップポインタ
 * @return	なし
 */
void CMySQLManager::SetGroupMap(std::unordered_map<int, CString>* MapGroup)
{
    try
    {
        m_con->setSchema(SCHEMA);
        sql::SQLString strSQL = "SELECT * FROM " + TABLE_GROUP; //グループテーブルから全取得

        // クエリを実行
        std::unique_ptr<sql::PreparedStatement> pstmt(m_con->prepareStatement(strSQL));
        std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

        // 結果を処理
        if (res->next())
        {
            do
            {
                int iGroupID = 0;
                CString strGroupName = _T("");

                std::string strsqlCol = res->getString("Id");           // ID列の取得
                CString strCol = strConvertFromUTF8ToUTF16(strsqlCol);  // 型変換
                iGroupID = _ttoi(strCol);

                strsqlCol = res->getString("Name");             // 名称列の取得
                strCol = strConvertFromUTF8ToUTF16(strsqlCol);  // 型変換
                strGroupName = strCol;

                (*MapGroup)[iGroupID] = strGroupName;	// デリファレンスしてからインデックスにアクセス				
            } while (res->next());
        }
        else
        {
            AfxMessageBox(_T("Groupテーブルから何も取得できない"));
        }
    }
    catch (sql::SQLException& e)
    {
        // 実行できなかった
        ErrSQLException(e);
    }
}
  • ~16行目:SELECT文を実行します。
  • 26~28行目:Id列のデータを取得、int型へ。
  • 30~32行目:Name列のデータを取得。
  • 34行目:IdとNameをマップで紐づけます。

一番重要なのは34行目ですかね。
テーブルの列名も、ヘッダーに配列用意して変数化した方がいいのかなぁとは思いますが。
今回は極小規模システムなので直打ちで。

全く同じ要領で、項目テーブルも取得します。

/**
 * @fn
 * @brief	項目テーブルセット
 * @param	std::unordered_map<int, KoumokuValue>* マップポインタ
 * @return	なし
 */
void CMySQLManager::SetKoumokuMap(std::unordered_map<int, KoumokuValue>* MapKoumoku)
{
    try
    {
        m_con->setSchema(SCHEMA);
        sql::SQLString strSQL = "SELECT * FROM " + TABLE_KOUMOKU; //項目テーブルから全取得

        // クエリを実行
        std::unique_ptr<sql::PreparedStatement> pstmt(m_con->prepareStatement(strSQL));
        std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

        // 結果を処理
        if (res->next())
        {
            do
            {
                int iIKoumokuID = 0;
                int iGroupID = 0;
                CString strKoumokuName = _T("");
                CString strPrice = _T("");

                std::string strsqlCol = res->getString("Id");           // ID列の取得
                CString strCol = strConvertFromUTF8ToUTF16(strsqlCol);  // 型変換
                iIKoumokuID = _ttoi(strCol);

                strsqlCol = res->getString("Name");                     // 名称列の取得
                strCol = strConvertFromUTF8ToUTF16(strsqlCol);          // 型変換
                strKoumokuName = strCol;

                strsqlCol = res->getString("GroupId");                  // グループID列の取得
                strCol = strConvertFromUTF8ToUTF16(strsqlCol);          // 型変換
                iGroupID = _ttoi(strCol);

                strsqlCol = res->getString("Price");                    // 値段列の取得
                strCol = strConvertFromUTF8ToUTF16(strsqlCol);          // 型変換
                strPrice = strCol;

                (*MapKoumoku)[iIKoumokuID] = { iGroupID, strKoumokuName, strPrice };
            } while (res->next());
        }
        else
        {
            AfxMessageBox(_T("Koumokuテーブルから何も取得できない"));
        }
    }
    catch (sql::SQLException& e)
    {
        // 実行できなかった
        ErrSQLException(e);
    }
}

形が違うのは44行目だけですかね。
キーに対する値が構造体になっているだけです。

これで、グループおよび項目テーブルから取得処理はできました。

申請ダイアログ作成

マップ作製したはいいが、表示させる手段がまだないので、
申請ダイアログでも作って正しく表示されるか見てみます。

毎度同じく、
リソースビューからDialogを右クリック→リソースの追加→新規追加
でダイアログを追加します。

ダイアログIDはIDD_DIALOG_REQUESTにします。

今回必要なコントロールは、
コンボボックス1つ、ラジオボタン5つ、申請ボタン、キャンセルボタンです。
申請ボタンはデフォルトのOKボタンを流用します。

では、早速いい感じに配置。


今回はシンプル。
特筆することはないです。
コンボボックスをドロップダウンリストにすることを忘れないくらいですかね。
後、ラジオボタンの枠幅は大きめにとること。

IDD_DIALOG_REQUEST DIALOGEX 0, 0, 177, 176
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,150,50,14
    PUSHBUTTON      "キャンセル",IDCANCEL,100,150,50,14
    COMBOBOX        IDC_COMBO_REQUEST_GROUP,20,20,130,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP
    CONTROL         "Radio1",IDC_RADIO_REQUEST_1,"Button",BS_AUTORADIOBUTTON,20,40,130,10
    CONTROL         "Radio2",IDC_RADIO_REQUEST_2,"Button",BS_AUTORADIOBUTTON,20,60,130,10
    CONTROL         "Radio3",IDC_RADIO_REQUEST_3,"Button",BS_AUTORADIOBUTTON,20,80,130,10
    CONTROL         "Radio4",IDC_RADIO_REQUEST_4,"Button",BS_AUTORADIOBUTTON,20,100,130,10
    CONTROL         "Radio5",IDC_RADIO_REQUEST_5,"Button",BS_AUTORADIOBUTTON,20,120,130,10
END

最後にクラスを新規に作ります。

適当にコントロールをダブルクリック。
今回はこのようなクラス名やファイル名です。

クラスビューからCRequestを選択、
プロパティのオーバーライドからOnInitDialogをAddします。

マップ内容表示処理

まずリソースビューの画面から、
先ほど追加したコンボボックスと申請ボタンをダブルクリックして
押下時に呼ばれる関数を追加しておきます。

先ほどのIDだと、Request.hとRequest.cppに、
OnCbnSelchangeComboRequestGroup()とOnBnClickedOk()が追加されるはず。

Request.hにメンバ変数や関数を追加します。

private:
	CComboBox m_ComboGroup;
	std::unordered_map<int, CString> m_MapGroup;			// グループマップ
	std::unordered_map<int, KoumokuValue> m_MapKoumoku;		// 項目マップ
	std::unordered_map<CString, int, CStringHash, CStringEqual> m_MapGroupReverse;	//グループマップ逆引き用
	std::unordered_map<CString, int, CStringHash, CStringEqual> m_MapKoumokuReverse;	//項目マップ逆引き用

private:
	void SetRadio(int iGroupID);

public:
	void GetMap(std::unordered_map<int, CString> MapGroup, std::unordered_map<int, KoumokuValue> MapKoumoku);

メイン画面から12行目でマップを引き取り、3,4行目のメンバ変数にコピーします。

5行目はグループマップの逆引きです。
名称からIDを取得するマップになっています。
マップから名称を取得しコンボボックスにセットしたのち、
選択されたコンボボックスの名称からIDを取得したいためです。
unorderdなので順は保証されていませんので、
確実にそのグループ名称に対するIDを取得したいと考えた結果がこれです。
この逆引きのために、CStringHashやCStringEqualを用意しました。
6行目項目マップも同様。
項目名からIDを取得します。

つまりです。
管理者が違うIDで同じグループ名や項目名を作ってしまうと
この逆引き機能しないんですよねー
(´▽`*)

テーブルでunique設定とかした方がいいのかな?
できるのかな?

ま、そこは今回は考えないことにします。

SetRadioは、選ばれたグループによって表示させる項目を変化させます。

Request.cppで表示処理を書きます。

まずDoDataExchangeでメンバ変数とコンボボックスIDを紐づけます。

void CRequest::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_COMBO_REQUEST_GROUP, m_ComboGroup);
}

GetMapも作成します。

/**
 * @fn
 * @brief	マップゲット関数
 * @param	std::unordered_map<int, CString>		グループマップ
 *			std::unordered_map<int, KoumokuValue>	項目マップ
 * @return	なし
 */
void CRequest::GetMap(std::unordered_map<int, CString> MapGroup, std::unordered_map<int, KoumokuValue> MapKoumoku)
{
	m_MapGroup = MapGroup;
	m_MapKoumoku = MapKoumoku;
}

これでメンバマップにテーブルから取得したデータを引き継げました。

次いで、OnInitDialogでメンバマップからコンボボックスのセット。
ついでに逆引きマップも作成します。

/**
 * @fn
 * @brief	初期表示処理
 * @param	なし
 * @return	TRUE
 */
BOOL CRequest::OnInitDialog()
{
	CDialogEx::OnInitDialog();

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

	m_MapGroupReverse.clear();	//空なのはわかっているが、念のため。
	int iGroupIDFirst;	//最初のグループID
	BOOL bInitCheckTemp = TRUE;
	for (const auto& pair : m_MapGroup)
	{
		if (bInitCheckTemp == TRUE)
		{
			bInitCheckTemp = FALSE;
			iGroupIDFirst = pair.first;	//グループID取得
		}

		m_ComboGroup.AddString(pair.second);			//コンボボックスに追加
		m_MapGroupReverse[pair.second] = pair.first;	//逆引き追加
	}

	m_ComboGroup.SetCurSel(0);	// 先頭にセット
	SetRadio(iGroupIDFirst);	// 先頭にセットされたグループに対しての項目セット

	SetWindowText(_T("購入申請画面"));

	UpdateData(FALSE);

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

これで、コンボボックスの方は値がセットできました。

続いてラジオボタンの方、項目のセットです。

/**
 * @fn
 * @brief	項目セット関数
 * @param	int		グループID
 * @return	なし
 */
void CRequest::SetRadio(int iGroupID)
{
	m_MapKoumokuReverse.clear();	// 項目マップ逆引きを初期化

	int iKoumokuCnt = 0;				// 項目数
	CArray<CString> strKoumokuNameArray;
	CArray<CString> strKoumokuPriceArray;
	// 項目マップから名称や値段を取得、逆引きも作成。
	for (const auto& pair : m_MapKoumoku)
	{
		const KoumokuValue& value = pair.second;
		if (value.iGroupID == iGroupID)
		{
			iKoumokuCnt++;
			strKoumokuNameArray.Add(value.strKoumokuName);
			strKoumokuPriceArray.Add(value.strKoumokuPrice);
			m_MapKoumokuReverse[value.strKoumokuName] = pair.first;
		}
	}

	//全ラジオボタン非表示
	CButton* pButton1 = (CButton*)GetDlgItem(IDC_RADIO_REQUEST_1);
	pButton1->ShowWindow(SW_HIDE);
	CButton* pButton2 = (CButton*)GetDlgItem(IDC_RADIO_REQUEST_2);
	pButton2->ShowWindow(SW_HIDE);
	CButton* pButton3 = (CButton*)GetDlgItem(IDC_RADIO_REQUEST_3);
	pButton3->ShowWindow(SW_HIDE);
	CButton* pButton4 = (CButton*)GetDlgItem(IDC_RADIO_REQUEST_4);
	pButton4->ShowWindow(SW_HIDE);
	CButton* pButton5 = (CButton*)GetDlgItem(IDC_RADIO_REQUEST_5);
	pButton5->ShowWindow(SW_HIDE);

	//項目数によってラジオボタンを表示させる
	if (iKoumokuCnt >= 1)
	{
		pButton1->ShowWindow(SW_SHOW);
		pButton1->SetWindowText(strKoumokuNameArray.GetAt(0));

		if (iKoumokuCnt >= 2)
		{
			pButton2->ShowWindow(SW_SHOW);
			pButton2->SetWindowText(strKoumokuNameArray.GetAt(1));

			if (iKoumokuCnt >= 3)
			{
				pButton3->ShowWindow(SW_SHOW);
				pButton3->SetWindowText(strKoumokuNameArray.GetAt(2));

				if (iKoumokuCnt >= 4)
				{
					pButton4->ShowWindow(SW_SHOW);
					pButton4->SetWindowText(strKoumokuNameArray.GetAt(3));

					if (iKoumokuCnt >= 5)
					{
						pButton5->ShowWindow(SW_SHOW);
						pButton5->SetWindowText(strKoumokuNameArray.GetAt(4));
					}
				}
			}
		}
	}
}

これで、コンボボックスの名称に応じてラジオボタンが変化する…はず!!

後は、コンボボックスが変化するたびにSetRadioを呼応させます。

void CRequest::OnCbnSelchangeComboRequestGroup()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	int iIndex = m_ComboGroup.GetCurSel();	// 選択されたインデックス
	if (iIndex != CB_ERR)
	{
		CString strSelectedText = _T("");
		m_ComboGroup.GetLBText(iIndex, strSelectedText);	//選択されたグループ名称を取得

		// 逆引きマップからグループIDを取得
		auto it = m_MapGroupReverse.find(strSelectedText);
		if (it != m_MapGroupReverse.end())
		{
			int iGroupID = it->second;	//ID取得
			SetRadio(iGroupID);	// グループIDに対する項目をセット
		}
	}
}

最後に、このダイアログを表示させる処理です。

リソースビューからメイン画面(IDD_SUPPLYREQUESTMANAGER_DIALOG)を開き、
「申請」ボタンをダブルクリックし、押下時に呼ばれる関数を追加です。

次いでSupplyRequestManagerDlg.cppを開きます。

ヘッダーをインクルード。

#include "Request.h"

先ほど追加した関数に、
マップを渡す処理とダイアログを呼び出す処理を書きます。

void CSupplyRequestManagerDlg::OnBnClickedButtonMainRequest()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	CRequest dlgRequest;
	dlgRequest.GetMap(m_MapGroup, m_MapKoumoku);	//マップ渡し
	dlgRequest.DoModal();
}

これでビルドを実施します。

動作確認

exeを開く前に。
そもそもテーブルに何も入れていない状態なので、簡単にデータを作ります。

グループはこんな感じに。

文房具、飲み物、本…
チョイスめちゃくちゃですが、ご了承を。

続いて項目は。

文房具が4つ、飲み物が3つ、本が5つあります。
ユーザが選べるのは本のジャンルだけです。
なんの本が来るかはお楽しみ()

さて、exe起動です。
ログインすると。

おや、グループテーブルから取得時にエラーが出てました。
因みにそのすぐ後に行う項目テーブル取得は問題なかったです。

対応&動作確認

やっちまったな、と。

最初なんでエラーが出るのかわかりませんでした。
SELECTするテーブル名をuserにするとエラーは出てこなかったので、
スペルミスかなぁってずっとにらめっこしてました。

そして気づきました。

groupって予約語じゃん

はい、修正。
今回はSQL文ではなく、そもそもテーブル名を変数化している箇所を修正します。
MySQLManager.hに定義しました。

	//テーブル
	const sql::SQLString TABLE_GROUP = "`group`";

テーブル名をバッククォートで囲みました。

再度ビルド。

exeを起動させると、今度はエラーが生じませんでした。
テーブル名を考えるのも設計の一つですね。
やったことなかったので思わぬ躓きでした。

さて申請画面を開くと。

グループIDが3のBookが先頭にきてるし、
項目はグループIDが1のデータが表示されるし。

コンボボックスを開くと

逆順で入ってるように見えなくもない。
ただ、この後、コンボボックスを変更させると。

ちゃんとグループに対応する項目が表示されました。

つまり、初期表示がおかしいのと、なぜかグループIDが逆順で入ってきている?

ま、マップ使ってる時点で順は保証されてないはずなんですけどね。

対応&動作確認2

たまたま「逆順に見える」だけでしたね。
OnInitDialogでコンボボックスにAddStringするところで何が入っているか確認すると、
ちゃんとグループID順に入っていました。
つまり、Stationery→Drink→Bookの順です。

これも凡ミス。
コンボボックスのプロパティから対応可能です。

「並べ替え」がTrueになっていました。
これがTrueだと自動でアルファベット順になっちゃいます。
なのでFalseに変更。

コンボボックスにちゃんとグループID順にグループ名称が入る
→先頭のグループID(Stationery)から表示項目を決定
→コンボボックスをアルファベット順に変更
していたから、初期表示がおかしかったんですね。

コンボボックスって並び替えFalseの方がよくつかわれると思うんだけど、
デフォルトTrueの理由って何かあるのかな?

とりあえずこれだけで再度ビルト。

初期表示も、コンボボックスの中身も想定通りです。

終わりに

今回はグループと項目をテーブルから取得し、画面に表示させました。

ちょいちょい凡ミスが目立つところも。

次回はこの画面はいったん置き、
項目情報変更画面を作ります。

今回はここまで。

MFCで物品購入承認システム作成 8
前回までの話。MFCで物品購入承認システム作成 1MFCで物品購入承認システム作成 2MFCで物品購入承認システム作成 3MFCで物品購入承認システム作成 4MFCで物品購入承認システム作成 5MFCで物品購入承認システム作成 6MFCで物...

コメント

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