遗留问题
之前学校的一个学生工作部门需要做一个程序用来随取题目,因为时间紧张,我先用C语言写了一个命令行的程序
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main(void)
{
// 1------11,41------51 大问题
// 12-----40,小问题
int Big_Ques,Small_Ques_1,Small_Ques_2;
char choose;
Lable:
Big_Ques = 0;
Small_Ques_1 = 0;
Small_Ques_2 = 0;
choose = '\n';
srand((unsigned)time(NULL));
Big_Ques = rand()%21+1;
if(Big_Ques>11)
{
Big_Ques+=30;
}
while(Small_Ques_1 < 12)
{
Small_Ques_1 = rand()%31;
Small_Ques_1+=10;
}
while(Small_Ques_2 < 12 || Small_Ques_1 == Small_Ques_2)
{
Small_Ques_2 = rand()%31;
Small_Ques_2+=10;
}
printf("****************************************************\n");
printf(" 答辩题目随机抽选\n");
printf("****************************************************\n");
printf(" 第一道小问题是 %d\n",Small_Ques_1);
printf(" 第二道小问题是 %d\n",Small_Ques_2);
printf("----------------------------------------------------\n");
printf(" 大问题是 %d\n",Big_Ques);
printf("****************************************************\n");
printf("\n\n是否再次抽取问题?\n");
printf("<1 再次抽取>\n");
printf("<2 退出程序>\n");
while(choose == '\n')
{
choose = getchar();
getchar();
if(choose == '1')
{
system("cls");
goto Lable;
}
}
}
十分简陋,没有界面,就只是cmd黑黑的一块,而且也只是显示一个范围内随机数字,具体题目还需要人来对照数字查找,其实就是做了骰子,用到的也就是生成随机数字的函数。虽然满足了对方的需求,但也差强人意。这个程序还需更加完善。
新的工程
正好,有了目标,也完整地学了一下C#,用的是VisualStudio2017这个强大的IDE,设计窗体也十分方便,这样就开始工作。
先分析一下,描述一下基本功能,需要满足一个用户在窗口内通过按键来随机抽取题库中的题目的需要,这个题库用户可以自己添加或删减,再抽取题目后窗口上同时显示出题目内容。因为对方需要哈,窗口内会把题干和答案同时显示。
接下来再分析一下,需要什么样的基本配置呢,一个窗体,上面有按键和文本显示窗口,需要一个方便用户打开和修改内容的题库。窗体和控件就是用窗体设计器的工具箱里的控件就好了。题库我用了两个txt文本,比较简单粗暴哈,一个用来存放小题题目,一个用来存放大题题目,这也是甲方需要。
还需要程序有什么样的模块呢,首先窗体设计器,然后是文件模块用来读取文本里的题目,随机数字模块用来选择抽哪道题目。大概就这些。
可是我在这的时候犯难了,如果用户用的时候忘了带上题库,或者抽到的数字题库里没有怎么办,程序怎么知道在用户的操作后,题库里有多少题。因为我经常会用到调试单片机的一些上位机,也深刻的意识到有一个类似输出窗口那样给出提示信息的面板多么方便,所以可以也设计一个文本框来显示程序的运行状态,可以提醒用户发生什么异常以及给出推荐的解决办法,让程序更友好。
需要再设计一个清空按键,清空当前所有文本框内的内容,避免信息窗口信息累积过多。
最好再留一个按键用来提供帮助信息,使用户用起来更清晰更方便,同时也能标注一下程序的当前版本。
因为这个程序是要用在大屏幕上对下面的观众展示,要求上面的题目的字体要足够清楚,也就是足够大,而且内容显示完整,具体需要多少我打算再设计两个按键可以用来调整字体大小,怎么样的字号合适还是由用户来决定吧,我只是提供一个初始值和调整办法。
这样最终的窗口布局就明确了,其实也是陆陆续续后期反复修改得到的:
初步设想的程序的流程,就是运行程序,然后程序自检题库时候存在,同时检查题库内的题目个数,如果出现任何异常,报告异常,并给出可能有效的解决方案,然后就是用户按下按键抽题,如果操作成功,报告成功和题目序号可方便用户管理,如果操作失败也给出解决方案。
背后代码
列好需求就可以着手准备了,先写好底层的组件,设计了两个类,一个是非静态的File类封装操作题库文件的一些方法,同时可以设置属性,封装为只读保护,另一个也是非静态的RandomNum类,里面可能只有一个静态的方法,生成一个给定范围内的随机数字的方法,其实C#里已经有很方便的随机数生成函数,我这里也封装为一个类,方便以后的功能开发,中了面向对象毒,haha。
File类里设置为非静态用来对应着两个题库来实例化,可设置的属性有文件名,题库内题目个数,对应文件检索时的异常信息,都用只读保护起来,因为他们的内容只和程序的运行有关,不能随意改动,然后有文件操作的方法,包括检索题库题目个数的方法,抽取题库中某一道题的方法。文件操作的实现就不详细展开了,采用教科书式方法,具体可以参考我之前一篇C#文件操作的博文,博文链接见文末。
检索题目数的方法将题目个数返回,将提示信息用out参数导出,在类的初始化里被调用
抽取题目的方法,将题目内容返回,同样将提示信息用out参数导出,另一个参数是想要抽取的题目序号,将在按键按下的事件响应中被调用,这里的题目序号没有做保护,是因为题目序号将由随机数产生的函数产生,这个数自身就在一个合理的范围内
这里还写了一个写入题目的方法,还没有利用起来,预留在这里,方便以后的功能拓展
public class File
{
/// <summary>
/// 文件名,只读属性,只能在初始化时定义
/// </summary>
private string file_name;
public string File_name { get { return file_name; } }
/// <summary>
/// 文件内文本行数,只读属性,在初始化时通过扫描获得
/// </summary>
private int file_linenum;
public int File_Linenum { get { return file_linenum; } }
/// <summary>
/// 文件操作的提示信息,用以观察运行状态
/// </summary>
private string file_statuMes;
public string File_StatuMes { get { return file_statuMes; } }
/// <summary>
/// 初始化,定位文件
/// </summary>
/// <param name="Initname"></param>
public File(string Initname)
{
this.file_name = Initname;
this.file_linenum = this.GetFileLine(out file_statuMes);
}
/// <summary>
/// 方法,获取文件当中的题目个数,仅初始化时调用
/// </summary>
/// <param name="Message"></param>
/// <returns></returns>
private int GetFileLine(out string Message)
{
int LineNum = 0;
try
{
FileStream aFile = new FileStream(this.file_name, FileMode.Open);
StreamReader sr = new StreamReader(aFile);
sr.ReadLine();
LineNum = 1;
//Read data in line by line.
while (sr.ReadLine() != null)
{
sr.ReadLine();
LineNum++;
}
sr.Close();
Message = "操作成功: 成功检查文件\n\r\n\r\n\r";
Message += $"题库中存在的题数目为{LineNum}\n\r\n\r\n\r";
return LineNum;
}
catch
{
Message = "操作失败: 无法读取文件,这可能是未将题库文件正确放置或正确命名导致的\n\r\n\r\n\r";
return LineNum;
}
}
/// <summary>
/// 方法,获取文件当中的第几道题目,按键响应时调用
/// </summary>
/// <param name="AimLine">目标题目</param>
/// <param name="item">输出信息</param>
/// <returns>返回题目内容</returns>
public string ReadFile(int AimLine ,out string item)
{
int Now_Line = 1;
string Result_Line;
try
{
FileStream aFile = new FileStream(file_name, FileMode.Open);
StreamReader sr = new StreamReader(aFile,System.Text.Encoding.UTF8);
aFile.Seek(0, SeekOrigin.Begin);
Result_Line = sr.ReadLine();
while (sr.ReadLine() != null)
{
if (AimLine == Now_Line)
{
item = "操作成功: 成功抽取题目\n\r\n\r\n\r";
sr.Close();
if (Result_Line.Length < 5)
{
return "抽取到的题目为空,可能与题库格式不正确有关,请重试或检查题库内部书写是否符合要求,正确格式参考右下角“帮助与版本”";
}
return Result_Line;
}
else
{
Now_Line++;
Result_Line = sr.ReadLine();
}
}
item = "操作成功: 成功抽取题目\n\r\n\r\n\r";
sr.Close();
if (Result_Line.Length < 5)
{
return "抽取到的题目为空,可能与题库格式不正确有关,请重试或检查题库内部书写是否符合要求,正确格式参考右下角“帮助与版本”";
}
return Result_Line;
}
catch
{
item = "操作失败: 未能获得题目,这可能是未将题库文件正确防置或正确命名导致的\n\r\n\r\n\r";
return ("Read Failed(by StreamWriter_Class): 读取错误,从题库提取题目时发生错误" );
}
}
/// <summary>
/// 方法,预留接口,向文件内写入
/// </summary>
/// <returns></returns>
public string WriteFile()
{
try
{
FileStream aFile = new FileStream(file_name, FileMode.Create);
StreamWriter sw = new StreamWriter(aFile);
bool truth = true;
// Write data to file.
sw.WriteLine("Hello to you.");
sw.WriteLine("It is now {0} and things are looking good.", DateTime.Now.ToLongDateString());
sw.Write("More than that,");
sw.Write(" it’s {0} that C# is fun.", truth);
sw.Close();
return "Write Successful(by StreamWriter_Class): AimFile Data.txt";
}
catch (IOException e)
{
Func<string> ErrorMessage = new Func<string>(e.ToString);
return "Write Failed(by StreamWriter_Class): " + ErrorMessage;
}
}
}
RandomNum类中有实例后的属性就是它的大小,包含一个方法来返回随机数,参数是随机数产生的上下界,内部还是调用的C#随机数字产生的函数。这个随机数字其实还是具有一定的统计规律,但在这样的题目环境下将不明显
需要的模块准备好后就可以着手使用了
public class RandomNum
{
/// <summary>
/// 所得到的随机数字,只读属性,只能通过随机算法获得
/// </summary>
private int resultnum;
public int ResultNum { get { return resultnum; } }
/// <summary>
/// 随机数实例初始化,需给定随机数产生范围
/// </summary>
/// <param name="Range_Max">随机数产生的范围上限</param>
/// <param name="Range_Min">随机数产生的范围下限</param>
public RandomNum(int Range_Max, int Range_Min)
{
resultnum = GetARandomNum(Range_Max, Range_Min);
}
/// <summary>
/// 获取随机数的方法
/// </summary>
/// <param name = "Range_Max" > 随机数产生的范围上限 </ param >
/// < param name="Range_Min">随机数产生的范围下限</param>
/// <returns>产生的随机数</returns>
public static int GetARandomNum(int Range_Max, int Range_Min)
{
Random ran = new Random();
return ran.Next(Range_Min,Range_Max + 1);
}
}
程序的主体写在了主窗口的初始化后,将文件类设置为主窗口的两个字段,分别实例化后就相当于和题库绑定在一起了,通过文件类下的方法检索。
File file_Bigproblem = new File("大题题库(和程序必须在同一目录下).txt");
File file_Smallproblem = new File("小题题库(和程序必须在同一目录下).txt");
加载初始化信息
//题目初始化,检查题库能否顺利打开,并检查题目数量,为随机数的产生准备
Test_MessageShow.Text = "小题题库检索......\r\n\r\n";
Test_MessageShow.Text += file_Smallproblem.File_StatuMes;
Test_MessageShow.Text += "大题题库检索......\r\n\r\n";
Test_MessageShow.Text += file_Bigproblem.File_StatuMes;
然后添加两个按键来实现抽选大题和抽选小题。
/// <summary>
/// 抽题小题按键按下响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Click_Small(object sender, EventArgs e)
{
String getMessage;
int QuestionLine = RandomNum.GetARandomNum(file_Smallproblem.File_Linenum, 1);
Text_Show.Text = file_Smallproblem.ReadFile(QuestionLine, out getMessage);
Test_MessageShow.Text += "小题题库检索......\r\n\r\n";
Test_MessageShow.Text += getMessage;
//确保检索通过,比较粗糙
if (file_Smallproblem.File_Linenum > 5)
{
Test_MessageShow.Text += $"抽到的小题题目号为{QuestionLine}\r\n\r\n\r\n";
}
}
/// <summary>
/// 抽题大题按键按下响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Click_Big(object sender, EventArgs e)
{
String getMessage;
int QuestionLine = RandomNum.GetARandomNum(file_Bigproblem.File_Linenum, 1);
Text_Show.Text = file_Bigproblem.ReadFile(QuestionLine, out getMessage);
Test_MessageShow.Text += "大题题库检索......\r\n\r\n";
Test_MessageShow.Text += getMessage;
//确保检索通过,比较粗糙
if (file_Bigproblem.File_Linenum > 5)
{
Test_MessageShow.Text += $"抽到的大题题目号为{QuestionLine}\r\n\r\n\r\n";
}
}
清空按键的响应
/// <summary>
/// 清空展示栏按键响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Clear(object sender, EventArgs e)
{
Test_MessageShow.Text = "";
Text_Show.Text = "";
}
文本框字号调整按键
/// <summary>
/// 文本框增大字号按键响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_SmallSizePlus_Click(object sender, EventArgs e)
{
SizeofSmallShow++;
Text_Show.Font = new System.Drawing.Font("楷体", SizeofSmallShow);
}
/// <summary>
/// 文本框减小字号按键响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_SmallSizeSub_Click(object sender, EventArgs e)
{
SizeofSmallShow--;
Text_Show.Font = new System.Drawing.Font("楷体", SizeofSmallShow);
}
帮助与版本按键的响应,就是弹出一个窗口,上面有一些想要嘱咐给用户的事情
/// <summary>
/// 帮助与版本按键响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
帮助与版本 helpForm = new 帮助与版本();
helpForm.Show(this);
}
遇到的问题
在设置帮助与版本这个按键的时候卡了很久,其实可以在工程内新建一个窗口,然后通过调用show出来
在设计消息窗口时发现当信息累积到文本框放不下时,设置的滚动条并不会自动的滑倒最下端,这样也会对用户造成不便,这里借鉴了网上查到的一种方法,也是很巧妙地通过事件触发就解决了这个问题
/// <summary>
/// 实现文本框滚动条自动滚动,文本框的事件触发
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Test_MessageShow_TextChanged(object sender, EventArgs e)
{
Test_MessageShow.SelectionStart = Test_MessageShow.Text.Length;
Test_MessageShow.ScrollToCaret();
}
在最后生成.exe应用程序后,我把它兴高采烈地同题库复制出来,看看作为收到作品的用户能不能正常使用呢,结果不行,程序没反应,窗口都没弹出来,查一下原因,结果是因为我把文件操作的相关类和方法封装在了类库里,一个dll文件,只迁移应用程序而缺少配置文件肯定行不通啊,我把原来和它在同一目录下dll配置文件也拷贝出来,运行正常。那这也不行,用户像我一样忘了这个dll文件,也以为只要exe就能运行怎么办,一看程序没有响应,一头雾水不知道问题出在哪,毕竟用户不像开发者那么清除。不行,当程序发生问题时应该给出提示,这样在程序一开始就添加了异常处理
try
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MianForm());
}
catch (Exception)
{
MessageBox.Show("程序未能正常运行,可能是缺少必要的配置文件“ClassFileOperation.dll”所导致的", "严重错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
}
这样整个程序就算写好了
实验效果还不错