質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

Q&A

解決済

4回答

2565閲覧

【Excel VBA】四択クイズのプログラムで重複した問題を出題させないようにしたい

koburon

総合スコア31

VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

0グッド

0クリップ

投稿2023/09/19 08:30

編集2023/09/21 06:28

前提

以下のサイトに記載のVBAコードで、四択問題を出し、正誤の結果を記録するプログラムを作成します。

試験勉強に最適な「Excel 4択クイズ実行プログラム」

プログラムの概要は以下の通りです。

  1. Sheet1に問題と選択肢、解説を入力する。(A列:問題番号、B列:問題文、C列:選択肢(正答)、D~F列:選択肢(誤答)、G列:解説文
  2. Sheet2のボタンを押してプログラムを実行するとフォームが表示される。
  3. 「スタート」をクリックする。
  4. 入力した問題がランダムに実行される。選択肢の位置もランダム表示される。
  5. 4つの選択肢ボタンを押して答えを選ぶ。
  6. 正答か誤答かフィードバックされる。(入力していれば、解説も表示される)
  7. 次の問題に進むかどうか聞かれる。
  8. 「はい」→④に戻り、くり返す。「いいえ」→出題を終了する。
  9. 日付、正答率、問題番号と正誤を記録する。Sheet3の「データ処理」ボタンを押すと問題番号順に正誤の結果が並び替えられる。重複データは削除され、正答と誤答どちらを優先するかも変更できる。

作成したフォームは下の画像の通りです。

イメージ説明
オレンジは、各要素のオブジェクト名や大きさ等を示しています。このオブジェクトを元にコードを入力します。

実現したいこと

掲載されたコードの通りに作成してプログラムは問題なく実行できましたが、実際に使ってみると、同じ問題が何度も出題される上、⑧で「いいえ」を選択しないとプログラムを終了できない、と不便さを感じました。
そこで今度は⑧と⑨の手順の間に次の手順を追加したいです。

  1. 次の問題に進む時に既出の問題を重複して出題しないようにする
  2. 全ての問題が出題されたら、出題を強制終了する

該当のソースコード

フォーム内に記載したコードは以下の通りです。「フォームの実行」と「データ処理」のコードはここでは省略します。

VBA

1Option Explicit 2Dim CorrectAns, CmntRow 3 4Private Sub UserForm_Initialize() 5 info.Visible = False 6End Sub 7 8Private Sub ToggleButton5_Click() 9 setQuizData 10 11'保存用シートへのデータ貼りつけ用の最終列取得の次の列i 12 Dim i 13 i = Sheet3.Cells(1, Columns.Count).End(xlToLeft).Column 14 If Sheet3.Cells(1, i).Value <> "" Then i = i + 1 'A1が空白ならiを1とする 15 16'記録用シートに日付を入力する 17Sheet2.Range("A1").Value = Date 18Sheet2.Range("B1").Value = "%" '後に正答率を入力する 19 20Do 21 While info.Visible = False 22 DoEvents 23 Wend 24 25 Dim nextQuiz 26 nextQuiz = MsgBox("次の問題に進みますか?", vbInformation + vbYesNo) 27 If nextQuiz = vbYes Then 28 info.Visible = False 29 setQuizData 30 Else 31 Exit Do 32 End If 33 34 Loop 35 Sheet2.Range("A1").CurrentRegion.Copy 'Sheet2のデータをコピー 36 'Sheet3に貼りつけ 37 Sheet3.Cells(1, i).PasteSpecial Paste:=xlPasteValues, _ 38 Operation:=xlNone, SkipBlanks:=False, Transpose:=False 39 Sheet3.Cells(1, i).NumberFormatLocal = "mm/dd" '表示形式を00月00日へ 40 Sheet3.Cells(1, i + 1).NumberFormatLocal = "0%" '表示形式をパーセントへ 41 Sheet2.Cells.Clear '記録用シートの初期化 42 43 Call getAverage(i) 44 45 MsgBox "問題集を終了します", vbInformation + vbOKOnly 46 Unload Me 47End Sub 48 49Private Sub getAverage(ByVal lBeginCol As Long) 50 51 Const TARGET_SHEET_NAME As String = "Sheet3" 52 Const COL_OFFSET As Long = 2 53 Dim sHeader As String 54 Dim lCol As Long 55 Dim lEndRow As Long 56 Dim lTargetCol As Long 57 58 lCol = lBeginCol 59 60 With ThisWorkbook.Worksheets(TARGET_SHEET_NAME) 61 sHeader = .Cells(1, lCol).Value 62 63 Do Until sHeader = "" 64 lEndRow = .Cells(1, lCol).End(xlDown).Row 65 66 lTargetCol = lCol + 1 67 68 .Cells(1, lTargetCol).Value = WorksheetFunction.Average(.Range(.Cells(2, lTargetCol), .Cells(lEndRow, lTargetCol))) 69 70 lCol = lCol + COL_OFFSET 71 72 sHeader = .Cells(1, lCol).Value 73 Loop 74 End With 75End Sub 76 77Private Sub setQuizData() 78 79 Randomize '乱数ジェネレータを初期化 80 Dim rowNo 81 rowNo = Int(Rnd * Sheet1.UsedRange.Rows.Count + 1) 82 quizText.Text = Sheet1.Cells(rowNo, 2) 83 CmntText.Text = "" 84 85'rowNoは問題の行数 86'解説を表示するためにrowNoを記録しておく 87 CmntRow = rowNo 88 89'問題ナンバーを入力する行番号mを定義 90Dim m 91 m = Sheet2.Cells(Rows.Count, "A").End(xlUp).Row + 1 92 ans1.Value = False 93 ans2.Value = False 94 ans3.Value = False 95 ans4.Value = False 96 97 ans1.Caption = "" 98 ans2.Caption = "" 99 ans3.Caption = "" 100 ans4.Caption = "" 101 102'変数の説明 103'ansFlag: いくつ選択肢を設定したのかを記憶しておく箱 104'ansNo: 1から4の間で発生させた乱数の値を記憶しておく箱 105'colNo: Sheet1の3列目から6列目に格納されている選択肢の、何番目までを設定したのかを記憶しておく箱 106 107 Dim ansFlag, ansNo, colNo 108 ansFlag = 0 109 ansNo = 0 110 colNo = 3 111 While ansFlag < 4 'ansFlagが4より小さいあいだ処理をくり返す 112 ansNo = Int(Rnd * 4 + 1) '0~1までの乱数Rnd に4をかけ、1を足し、小数点以下を切り捨てるInt 113 If UserForm1.Controls("ans" & ansNo).Caption = "" Then 114 UserForm1.Controls("ans" & ansNo).Caption = Sheet1.Cells(rowNo, colNo) 115 ansFlag = ansFlag + 1 116 117 Sheet2.Range("A" & m).Value = Sheet1.Cells(rowNo, 1) '記録シートに問題番号を入力 118 119 '正答(Sheet1の3列目)がどのトグルボタンに設定されたかをCorrectAnsに記憶 120 If colNo = 3 Then 121 CorrectAns = ansNo 122 End If 123 colNo = colNo + 1 124 End If 125 Wend 126 127End Sub 128 129Private Sub answerJudg(tName) 130Dim n 131 n = Sheet2.Cells(Rows.Count, "B").End(xlUp).Row + 1 132 133 If UserForm1.Controls("ans" & tName).Value = False Then 134 Exit Sub 135 End If 136 137 If CorrectAns = tName Then 138 info.Caption = "○ 正解" 139 CmntText = Sheet1.Cells(CmntRow, 7) 140 Sheet2.Range("B" & n).Value = "1" '記録用シートに正答を記録する 141 Else 142 info.Caption = "× 不正解" 143 CmntText = Sheet1.Cells(CmntRow, 7) 144 Sheet2.Range("B" & n).Value = "0" '記録用シートに誤答を記録する 145 146 End If 147 info.Visible = True 148End Sub 149 150Private Sub ans1_Click() 151 answerJudg (1) 152End Sub 153 154Private Sub ans2_Click() 155 answerJudg (2) 156End Sub 157 158Private Sub ans3_Click() 159 answerJudg (3) 160End Sub 161 162Private Sub ans4_Click() 163 answerJudg (4) 164End Sub

試したこと

重複してしまう原因はsetQuizData()内のRandomizeでルーチンを初期化しているのが原因かと思います。
次の問題に進む時、問題の参照にiを使わず、rowNoにセットする値を工夫する事で、未出題か出題済みか判定するような処理を書けば、問題の重複を避けられるかな、と考えています。
ただ参考になるサイトが見つからず具体的にどんなコードを書けばいいかわからない状況です。
回答となるコードを丸ごといただければ勿論うれしいですが、それだけでは今後の勉強にならないので、考え方やヒントをいただければ幸いです。
よろしくお願いいたします。

追記

いただいた回答を元にコードをいくつか修正しました。途中までの重複しない出題は達成できましたが、全問を出題した後の処理が進まない状態です。

VBA

1Private Sub ToggleButton5_Click() 2 Dim end_flag As Boolean 3 Call setQuizData(end_flag) 4 5'保存用シートへのデータ貼りつけ用の最終列取得の次の列i 6 Dim i 7 i = Sheet3.Cells(1, Columns.Count).End(xlToLeft).Column 8 If Sheet3.Cells(1, i).Value <> "" Then i = i + 1 'A1が空白ならiを1とする 9 10'記録用シートに日付を入力する 11Sheet2.Range("A1").Value = Date 12Sheet2.Range("B1").Value = "%" '後に正答率を入力する 13 14Do 15 While Info.Visible = False 16 DoEvents 17 Wend 18 19 Dim nextQuiz 20 nextQuiz = MsgBox("次の問題に進みますか?", vbInformation + vbYesNo) 21 If nextQuiz = vbYes Then 22 Info.Visible = False 23 Call setQuizData(end_flag) 24 25 ElseIf end_flag = True Then 26 Exit Do 27 28 Else 29 Exit Do 30 End If 31 32 Loop 33 Sheet2.Range("A1").CurrentRegion.Copy 'Sheet2のデータをコピー 34 'Sheet3に貼りつけ 35 Sheet3.Cells(1, i).PasteSpecial Paste:=xlPasteValues, _ 36 Operation:=xlNone, SkipBlanks:=False, Transpose:=False 37 Sheet3.Cells(1, i).NumberFormatLocal = "mm/dd" '表示形式を00月00日へ 38 Sheet3.Cells(1, i + 1).NumberFormatLocal = "0%" '表示形式をパーセントへ 39 Sheet2.Cells.Clear '記録用シートの初期化 40 41 Call getAverage(i) 42 43 MsgBox "問題集を終了します", vbInformation + vbOKOnly 44 Unload Me 45 46End Sub

VBA

1Private Sub setQuizData(ByRef end_flag As Boolean) 2 end_flag = True 3 4 '空いている行をrowNoに設定する。空き行がない場合は、-1をrowNoに設定する。 5 Dim rowNo As Long 6 Call get_row_number(rowNo) 7 8 'すべて 9 If rowNo = -1 Then 10 Exit Sub 11 End If 12 13 quizText.Text = Sheet1.Cells(rowNo, 2) 14 CmntText.Text = "" 15 16 'rowNoは問題の行数 17 '解説を表示するためにrowNoを記録しておく 18 CmntRow = rowNo 19 20 21 '問題ナンバーを入力する行番号mを定義 22 Dim m 23 m = Sheet2.Cells(Rows.Count, "A").End(xlUp).Row + 1 24 25 ans1.Value = False 26 ans2.Value = False 27 ans3.Value = False 28 ans4.Value = False 29 30 31 ans1.Caption = "" 32 ans2.Caption = "" 33 ans3.Caption = "" 34 ans4.Caption = "" 35 36'変数の説明 37'ansFlag: いくつ選択肢を設定したのかを記憶しておく箱 38'ansNo: 1から4の間で発生させた乱数の値を記憶しておく箱 39'colNo: Sheet1の3列目から6列目に格納されている選択肢の、何番目までを設定したのかを記憶しておく箱 40 41 Dim ansFlag, ansNo, colNo 42 ansFlag = 0 43 ansNo = 0 44 colNo = 3 45 While ansFlag < 4 'ansFlagが4より小さいあいだ処理をくり返す 46 ansNo = Int(Rnd * 4 + 1) '0~1までの乱数Rnd に4をかけ、1を足し、小数点以下を切り捨てるInt 47 If UserForm1.Controls("ans" & ansNo).Caption = "" Then 48 UserForm1.Controls("ans" & ansNo).Caption = Sheet1.Cells(rowNo, colNo) 49 ansFlag = ansFlag + 1 50 51 Sheet2.Range("A" & m).Value = Sheet1.Cells(rowNo, 1) '記録シートに問題番号を入力 52 53 '正答(Sheet1の3列目)がどのトグルボタンに設定されたかをCorrectAnsに記憶 54 If colNo = 3 Then 55 CorrectAns = ansNo 56 End If 57 colNo = colNo + 1 58 End If 59 Wend 60 61 end_flag = False 62End Sub

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

ToggleButton5_ClickのDo~Loopですが以下のようにしてください。
Call setQuizData(end_flag)
の次の行に
If end_flag = True Then
info.Visible = True
Exit Do
End If

を入れます。
前の回答ではinfo.Visibleを考慮していませんでした。
exit Doするときは、info.VisibleがTrueになっていないといけないようなので、そのようにしました。

VBA

1 Do 2 While info.Visible = False 3 DoEvents 4 Wend 5 6 Dim nextQuiz 7 nextQuiz = MsgBox("次の問題に進みますか?", vbInformation + vbYesNo) 8 If nextQuiz = vbYes Then 9 info.Visible = False 10 11 Call setQuizData(end_flag) 12 If end_flag = True Then 13 info.Visible = True 14 Exit Do 15 End If 16 Else 17 Exit Do 18 End If 19 20 Loop 21

投稿2023/09/21 07:32

編集2023/09/21 07:45
tatsu99

総合スコア5533

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

koburon

2023/09/21 08:03

回答ありがとうございます。最終問題を出題後にエラー無く終了できました。理想通りのプログラムが実行できました。本当にありがとうございました。こちらをベストアンサーとさせていただきます。
guest

0

空き行を返す関数です。

VBA

1'空き行を返す関数 2Private Sub get_row_number(ByRef rowNo As Long) 3 Dim wrow As Long 4 Dim wrow_save As Long 5 wrow = Int(Rnd * Sheet1.UsedRange.Rows.Count + 1) 6 '乱数で取得した行番号が空きなら、その行番号を返す 7 If Sheet1.Cells(wrow, "Z").Value = "" Then 8 Sheet1.Cells(wrow, "Z").Value = 1 9 rowNo = wrow 10 Exit Sub 11 End If 12 wrow_save = wrow 13 '乱数で取得した行番号の次から下へ最後まで空き行を検索する 14 For wrow = wrow_save + 1 To Sheet1.UsedRange.Rows.Count 15 '空き行があれば、その行番号を返す 16 If Sheet1.Cells(wrow, "Z").Value = "" Then 17 Sheet1.Cells(wrow, "Z").Value = 1 18 rowNo = wrow 19 Exit Sub 20 End If 21 Next 22 '乱数で取得した行番号の前から上へ先頭まで空き行を検索する 23 For wrow = wrow_save + -1 To 1 Step -1 24 '空き行があれば、その行番号を返す 25 If Sheet1.Cells(wrow, "Z").Value = "" Then 26 Sheet1.Cells(wrow, "Z").Value = 1 27 rowNo = wrow 28 Exit Sub 29 End If 30 Next 31 '空き行がないので、-1を返す 32 rowNo = -1 33End Sub 34

呼び出し側

VBA

1Dim rowNo As Long 2Call get_row_number(rowNo) 3'空いている行がrowNoに設定されます。空き行がない場合は、-1がrowNoに設定されます。

UserForm_Initializeに以下の行を追加します。

VBA

1 'Z列をクリアする 2 Sheet1.Range("Z1:Z" & Sheet1.UsedRange.Rows.Count).ClearContents 3 Randomize '乱数ジェネレータを初期化 4

投稿2023/09/20 09:22

tatsu99

総合スコア5533

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

koburon

2023/09/21 01:13

回答ありがとうございます。UserForm_Initialize()の中にZ列のクリアを追記、空き行を返す関数のSubを追加しました。呼び出し側のコードはsetQuizData()内の以下の80~81行から差し替える、という認識でよろしいでしょうか。 ーーーーーーーーーーー Dim rowNo rowNo = Int(Rnd * Sheet1.UsedRange.Rows.Count + 1) ーーーーーーーーーーー 試しに差し替えて実行したところ、重複せず出題されるようになりましたが、最後の問題を解答すると、エラーが生じてしまいました。コードの挿入する場所が間違っているのでしょうか。
tatsu99

2023/09/21 02:15

はい。あってます。 79行のRandomize '乱数ジェネレータを初期化 は、削除してますね。(UserForm_Initializeへ移動) 「最後の問題を解答すると、エラーが生じてしまいました。」ということですが、 call Call get_row_number(rowNo)の結果 割当行がなくなると、最後にはrowNoに-1が設定されます。 このケースについては、触れていませんでしたが、このケースを考慮する必要があります。 つまり、「全ての問題が出題されたら、出題を強制終了する」ことの実現になります。 これに関しては、少々厄介です。 ToggleButton5_Click()の29行 setQuizDataを呼び出した後に、全部問題が出題された時の判定を追加する必要があります。 そして、setQuizData側では、 ①正常に終了、②全部問題が出題された為に終了 のどちらかを返すようにする必要があります。 Private Sub setQuizData()を Private Sub setQuizData(ByRef end_flag As Boolean) に変えます。 setQuizDataの最初に end_flag = True を追加します。 Dim rowNo As Long Call get_row_number(rowNo) の後に If rowNo = -1 Then Exit Sub を追加します。 setQuizDataの最後に end_flag = False を追加します。 呼び出し側(ToggleButton5_Click)ですが ToggleButton5_Clickの先頭へ dim end_flag as Boolean を追加します。 29行のsetQuizData を Call setQuizData(end_flag) If end_flag = True Then Exit Do に変えます。 不明点1 9行目にsetQuizDataがありますが、これは必要なのでしょうか。 もし、必要であれば、 Call setQuizData(end_flag) にかえてください。 不明点2 Sheet1の1行目と最終行は、出題の対象行にして良いのでしょうか。 それとも、1行目と最終行を除いて、出題するのでしょうか。
koburon

2023/09/21 06:29 編集

回答ありがとうございます。 不明点について回答いたします。 ・不明点1 →setQuizDataの文を抜いて実行すると、フォームのスタートボタンを押しても問題が始まりませんでした。コメント通り、Call setQuizData(end_flag)に変えると出題できるようになりました。 ・不明点2 →1行目と最終行も対象行です。なので、提示いただいたコードで1行目と最終行も出題するようにします。 コメントいただいたように修正し実行したところ、途中までは重複しないように出題されましたが、最終問題を解答した後に「はい」をクリックすると出題を終了できずそのまま固まってしまいました。 修正途中のそれぞれのコードを質問文に追記したので、ご面倒をおかけしますが確認していただきどこで入力ミスしているかコメントいただけないでしょうか。
guest

0

とりあえず、手っ取り早い方法は、2回目のrowNoは採用しないようにすることかと思います。
その為には、
1.Sheet1の空いている列(例えばZ列とします)を記憶用に使います。
2.UserForm_Initialize()の中で、Z列を全行分クリアしておきます。
3.乱数でrowNoを求めたとき、Z列のrowNo行が空白なら、その行を使用し、その行へ1をセットします。
4.乱数でrowNoを求めたとき、Z列のrowNo行が空白でないなら、その行は使えません。
その場合、以下の方法が考えられます。
1案.再度乱数を発行し、Z列のrowNo行が空白になるまで、繰り返す。
2案.そのrowNoから下へ空白の行を検索する。空白の行があれば、それをrowNo行として採用する。
最後の行まで検索しても、空白行がないなら、上へ向かって同様に検索する。空白行があれば、それをrowNo行として採用する。
先頭行まで検索しても、空白行がない場合は、全て使用済みということなので、出題を強制終了する。

個人的には2案を推奨します。(ランダム度が弱くなりますが、不要なループを繰り返さずに済みます)

投稿2023/09/19 10:19

tatsu99

総合スコア5533

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

koburon

2023/09/20 07:55 編集

回答ありがとうございます。今後問題数を増やしていく予定なのでループが少なく済む2案で進めたいと思います。試しに自力で条件分岐を入力し、コード84行目の位置に以下のように追加しました。 ーーーーーーーーーーー 'Z列のrowNo行が空白なら、セルに1をセット 'Z列のrowNo行が空白でないなら1つ下を検索、空白であれば1をセット 'Z列のrowNo行の1つ下が空白でないなら1つ上を検索、空白であれば1をセット '空白行がなくなったら出題を終了する If Sheet1.Cells(rowNo, 26) = "" Then Sheet1.Cells(rowNo, 26) = "1" ElseIf Sheet1.Cells(rowNo, 26) = "1" Then rowNo = rowNo + 1 ElseIf Sheet1.Cells(rowNo + 1, 26) = "1" Then rowNo = rowNo - 1 Else Exit Sub MsgBox "問題を終了します", vbInformation + vbOKOnly Unload Me End If ーーーーーーーーーーー エラーは生じなかったのですが、初期状態と同じく、同じ問題が何度も出題されてしまい、問題の終了も「いいえ」を押す必要があります。また、何問か解いていると問題文と選択肢が表示されないパターンがありました。これは1行目か最終行のオフセットを参照してしまったことが原因だと思いますが、1行目と最終行の場合は参照しないように制限することができるのでしょうか。直す点がどこかアドバイスいただけないでしょうか。
tatsu99

2023/09/20 09:14

'Z列のrowNo行が空白なら、セルに1をセット 'Z列のrowNo行が空白でないなら1つ下を検索、空白であれば1をセット 'Z列のrowNo行の1つ下が空白でないなら1つ上を検索、空白であれば1をセット '空白行がなくなったら出題を終了する Z列が空白でないなら、下へ空白でない行を最後まで探します。 それもないなら、上へ先頭まで探します。 探すのが、下へ1行、上へ1行だけのように見えます。 >1行目と最終行の場合は参照しないように制限することができるのでしょうか。 rowNoが1であれば、1行目なので、スキップする。 rowNoが最後の行なら、スキップする。ということをすれば、良いかと思います。 1行目と最後の行は、問題の対象外の行なのでしょうか?
tatsu99

2023/09/20 09:17

サンプルで空き行を返す関数を回答欄に書きます。 1行目と最終行も対象にしてあります。 1行目と最後の行は、問題の対象外の行であることが、明確になれば、その行はスキップするように後で変更すれば良いでしょう。
guest

0

考え方で言えば配列に問題(問題がある行番号)を格納して出題された問題(行番号)を配列から削除する
という感じでもできるかと思います。
ですがVBAで配列を扱うのは少し手間かもしれません。
追記
VBAには「Collection」というものがあったので配列じゃなくこちらを使えば簡単かもしれません。

投稿2023/09/20 05:08

編集2023/09/20 05:18
bebebe_

総合スコア520

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問