[更新 2016-11-18 9:15]

第二次更新的时候我统计了一下男、女和混合班在两个校区分别有几个班。

当时在处理的时候,根据老师的习惯,包含""字的则认为是男生班,而混合班数量比较好,剩下的是女生版(相信的看11-16的更新即可)。

但是在实际过程中,发现,这样子并不是完全可靠的,因为男生班里面,散打班是男生班,但是只有这一个是不一样的,其他都含有"男"字,而混合班也多了一些。

因此我又做了一个框,用于标明没有男字的男生班

同时,老师需要统计新生和老生分别男、女、混合班都有多少个,而我在第一次的处理中就已经分开新生老生了了(详细见下面的文章),刚好分开统计即可,因为一个label已经放不下统计的结果,我又做了一个只读的RichTextBox,来放最后的统计结果。

效果如下:

1

其实后来我觉得,在课程代码中再加一个字段,说明是男生班还是女生班或者是混合班,才是最好的处理方式。

但是懒得弄了 - -已经搞定了反正~~~


更新分割线


[更新 2016-11-16 14:57]

内容:

要求课程代码里面必须都是文本格式,因为一些课程是不分男女的,也就是称为混合班,有好几个代码,因此需要使用 丶 这个字符来链接就像是下面这样子:

3

保存在dataset中的时候应该保存的是文本格式,而不是double(这是由于dataset中类型必须是一样的,第一字符如果是数字的话,第二个也会强制成为double类型的

增加了点功能:

老师需要在排好课后知道每个校区男生班、女生班、混合班各有多少个。

实现:

每次匹配到了课程代码后,就再进行一些匹配。

比较好的地方在于,老师排课使用的名称,男生班都很明显有个"",而混合班和女生班是没有的,因此只要在匹配课程代码成功时,判断是否包含"男"即可判定是不是男生班

对于混合班和女生班则不一样,女生班很多是不存在"女"的,但是我发现一个比较明显的地方在于,混合班实际上很少,也就几个。上面那个图中,通过拼接在一起的,都是混合班。

因此我做了一个richTextBox,把目前已经知道的混合班初始化进去,并且可以手动的输入,这样子如果不是男生班,则循环匹配是不是混合班,如果还不是混合班,那么肯定就是女生班了

效果如下:

1

 

2

代码:

在原来的基础上,我在git上新建了一个分支,把master作为现在更新的分支,而more分支则是之前的代码。

 

4

 


更新分割线


 

前述:

在[SXWTaxCaculation个税计算 http://www.ptbird.cn/sxwtaxcaculation-open-blog/ ]中曾经提到过主要原出发点是因为体育部老师找我能不能帮忙用代码处理排课时候的一些麻烦事情,然后就促生了个税计算http://sxw.ptbird.cn 这个软件以及我自己的授权方式。

因为个税计算Excel版本目前暂不开源,还要付费授权,因此相关的代码细节没办法很好的展现出来,而基于对个税计算Excel版本的开发经历,让本来觉得无法实现教师排课助手也实现出来了,而且根据实际的使用情况,也很好的结合了C#操作Excel两种方式,即OLEDB和Range Cell方式。

场景:

说一下场景,学校教务处排课主要是看课程代码,因此部分负责老师在进行排课的时候不能简单地写上课程名字,还要写好课程代码。

也就是像下面这个图:

1

 

而老师用的是简称,比如 男篮(高) 表示的是男篮提高班,还有大一的课程和大二、三、四是不一样的

因此每次排课老师在排课的过程中都要仔细仔细再仔细的查看代码保证没有错误,这就是一件很蛋疼的事情。

实际上这种和个税计算的催生原因是一样的,人工总是会出错,而且要检查好几遍,而代码很大程度上规避这种人为的出错几率,你可以认为程序肯定不是100%的正确但是绝对是99%的不会出错。

目前排好课的excel表格如下图:现在是没有写课程代码的。

2

 

实现:

使用C# winform来实现,根据实际使用的情况设计了一种比较合理的实现模式。

一、模拟课程代码数据库,oledb的实现

由于老师在排课过程中实际上有自己的命名习惯,比如会写简称,而这个简称实际上是固定的。

因此将排课过程中使用的课程简称和课程代码结合起来,使用excel模拟一个数据表是最直接的表达方式,对于后面课程代码的扩展也提供了很大的遍历。

模拟的数据表如下所示(可以说是一个标准的数据表,结构化的数据):

3

 

而恰好利用Oledb来操作excel,可以将这些课程代码标准的存储在DataSet中,oledb方式对于excel的操作方式就是相当于sql语句对于数据表的处理是一模一样的。

我们在这个过程中只需要用到查询即可。

主要的关键点如下:

1)正确的创建excel的链接,这一点很关键

我用的vs2015 需要引入 excel 15.0 object library 1.8,在com那里直接搜excel就能出来,针对excel 2003和excel 2007后的版本,有两种创建方式,详细见代码。

2)获取第一张sheet的名字,默认是处理第一张sheet,因为只有一个sheet,但是名字不一定就是sheet1

3)将所有的中文括号替换成英文括号

后来发现其实直接在excel中替换是最好的方式,课程代码就是强制要求都是英文括号的,像是女篮(高) 就强制使用英文。

代码片段加注释:


 /*****************************************************************/                      
                //获取课程代码部分 使用oledb方式进行操作
/*****************************************************************/
//获取文本内容
courseCodeFilePath = courseCodeFileTextBox.Text.ToString().Trim();
//创建连接 以便发生异常时关闭连接(建在try外)
OleDbConnection courseCodeOleConn = new OleDbConnection();
try
{
    //判断文件 2003还是2007 分别创建文件链接
    //创建连接,引用协议
    string courseCodeConn = "";
    if (this.courseCodeExtention == ".xls")
    {
        //2003(Microsoft.Jet.Oledb.4.0)
        courseCodeConn = string.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties='Excel 8.0;HDR=Yes;IMEX=2;'", this.courseCodeFilePath);
    }
    else
    {
        //2010(Microsoft.ACE.OLEDB.12.0)
        courseCodeConn = string.Format("Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties='Excel 12.0;HDR=Yes;IMEX=2;'", this.courseCodeFilePath);
    }

    //打开连接并执行sql语句,末尾需要关闭连接
    courseCodeOleConn = new OleDbConnection(courseCodeConn);
    courseCodeOleConn.Open();
    //获取所有的表 默认处理第一张表
    System.Data.DataTable courseTables = courseCodeOleConn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);
    //获取第一张表的名字用于查询
    this.courseCodeTable = courseTables.Rows[0]["TABLE_NAME"].ToString();
    // 输出状态
    statusLabel.Text = "从" + this.courseCodeTable + "表读取课程代码...";
    //执行sql查询功能,保存到dataset中    
    String sql = string.Format("SELECT * FROM  [{0}]", this.courseCodeTable);
    //创建查询语句
    OleDbDataAdapter oleAdapter = new OleDbDataAdapter(sql, courseCodeOleConn);
    //创建dataSet保存数据
    DataSet ds = new DataSet();
    //获得数据
    oleAdapter.Fill(ds, this.courseCodeTable);
    //对创建dataSet保存数据的遍历
    //将中文括号变成英文括号 方便匹配
    for (int i = 0; i < ds.Tables[0].Rows.Count; i++)
    {
        for (int j = 0; j < ds.Tables[0].Columns.Count; j++)
        {
            //MessageBox.Show(ds.Tables[0].Rows[i][j].ToString(), "提示框");
            ds.Tables[0].Rows[i][j]=ds.Tables[0].Rows[i][j].ToString().Replace("(", "(");
            ds.Tables[0].Rows[i][j]=ds.Tables[0].Rows[i][j].ToString().Replace(")", ")");
        }
    }
    //返回处理后的ds
    this.courseCodeDs = ds;

二、不规则的排课excel表的处理

说明一下,很多东西我都写死了,因为是针对性的使用,没必要做很多设置。

从上面那个图,可以很明显的看出,实际上,排课表肯定是无法使用oledb来进行处理的,太乱了。

因此针对排课表处理的主要注意点如下:

1、使用Range Cell[row,col]也就是所谓的数组模式处理课程表,修改、读取。

2、表对象的创建和遍历。

3、如果处理大一和大二、三课程代码的不同。

1、对表的遍历部分:

4

分析表可以看出很多单元格是合并的,比如我要看这个课程是大一还是大二三的简单的使用数组Cell[row,col]可能读出空的来,所以需要处理合并单元,保证读出的都是合并单元格的内容。

其实并不是很难,下面一段是我代码里的注释(个人习惯的注释风格):

处理每个表 因为只有松江区校区和延安路校区 因此这里就写死了,只有两个sheet 需要处理分别是1 2 没有0 非常蛋疼。。。,同样 在判断大一和大二的问题上 由于排课表是确定形式的 因此B列存在的是大一还是大二的问题。
[废弃] 而A列是上课的时间 因此也写死了 不去遍历 最高到L 同时 最长到34
[废弃] 因此在处理的过程中 直接处理的是 C-L 以及 1-40 这样子写死遍历
[更新] 本来打算使用B1来获取数据,但是发现存在合并的单元格,因此最后还是采用[1,2]的方式获取数据
[更新] 因此在本来 C-L 的循环变成了 3-12的循环 还是没有0
[更新] 同时循环的方式也进行了改变 不过 在判断是否是大一的问题上

获取大一还是打二三的方式如下,需要判断合并单元格:

//判断是新生还是老生
//根据上面写死的 其中 当前行的第2列是表示大一还是大二、三的
//因此根据 大一来判断新生否则是老生
Range tmpGradeRange = (Range)tmpTable.Cells[i, 2];
string tmpGrade = "";
//判断是否是合并单元格
if ((bool)tmpGradeRange.MergeCells)
{
    Range mergeArea = (Range)tmpTable.Cells[tmpGradeRange.MergeArea.Row, tmpGradeRange.MergeArea.Column];
    tmpGrade = mergeArea.Text.ToString();
} else
{
    tmpGrade = tmpGradeRange.Text.ToString();
}

 

其实对表的遍历没有什么针对表,两个循环就能解决,不过需要注意的是,读取表最好多读一些,将空白的跳过即可。

 

三、结合DataSet进行综合处理

遍历获取单元格的内容后就要结合DataSet进行数据的处理,我这里写的复杂度是O(n^3),这个复杂度没什么,因为课程也没有多少。

实际上在双重循环下在加一个DataSet的循环即可。

不过在如何匹配的问题上我还是费了些周折,后来就变成简单粗暴了。

下面是我的注视过程(又是蛋疼的注释,凑和着看吧):

//  课程代码结构很明显 从 0 开始 0-序号 1 代表课程名称 2代表新生 3代表老生代码
//  [废弃]如果课程中包含了课程代码中的文字 那么进行匹配
//  [废弃]针对有些时候没有写男女 加了一次判定 本列第三行 是代表了男生女生
//  [废弃][第一次更新]第二次判断 如果里面没有写明男女 则判断课程代码中包含 比如 攀岩(男)【代码】 而课程中是 攀岩 列标题写了男 这样子也可以匹配
/  [废弃][第一次更新]上面的做法容易产生 男 女 单个字符的匹配 因此加了个判定不能只是 男 女
//  [第二次更新] 发现如果排课中 字数多 包含 字数少 不能正确匹配 比如 男篮(高)最后匹配的是 男篮。
//  [第二次更新] 只要一个条件 那就是相等!!就是强制让你一样,不服咬我啊! 判断是否完全相等
// 日 le dog 看for循环下面的解释

//解释在这里如果不懂啥意思 可以看看下面再上来
// 发现存的时候 是 王:男篮(高) 因此是不能直接相等 因此需要去掉前两个 再来一个临时字符

//也就是老师排课的时候会写个王:前面两个字符,因此首先需要将所有的空格去掉,在去掉前两个字符


 string tmpCouse = "";
if (tmpText.Length >= 2)
{
    tmpText=tmpText.Replace(" ", "");
    tmpCouse =tmpText.Substring(2);
}

最后循环处理的排课的表的部分的代码如下所示:


/*****************************************************************/
//操作排课表  使用Cells[x, y]
/*****************************************************************/
// 输出状态
statusLabel.Text = "处理排课...";
try
{
//声明app
Microsoft.Office.Interop.Excel.Application courseApp = new Microsoft.Office.Interop.Excel.Application();
//让后台执行设置为不可见
courseApp.Visible = false;
Workbooks wbks = courseApp.Workbooks;
//获取文档
_Workbook _wbk = wbks.Add(courseFilePath);
//获取表
Sheets shs = _wbk.Sheets;
//处理每个表  因为只有松江区校区和延安路校区 因此这里就写死了
// 只有两个sheet 需要处理分别是1 2  没有0 非常蛋疼。。。
//同样 在判断大一和大二的问题上 由于排课表是确定形式的 因此B列存在的是大一还是大二的问题
//[废弃]  而A列是上课的时间 因此也写死了 不去遍历 最高到L 同时 最长到34  
//[废弃]  因此在处理的过程中 直接处理的是 C-L 以及 1-40 这样子写死遍历
//[更新]  本来打算使用B1来获取数据,但是发现存在合并的单元格,因此最后还是采用[1,2]的方式获取数据
//[更新]  因此在本来 C-L 的循环变成了 3-12的循环 还是没有0
//[更新]  同时循环的方式也进行了改变 不过 在判断是否是大一的问题上  
//可以判断值是否为空 进行跳过
for (int m = 1; m <= 2; m++)
{
//简单的循环处理 1代表松江 2代表延安路校区
string statusText = "";
//fefe
_Worksheet tmpTable = (_Worksheet)shs.get_Item(m);
if (m == 1)
{
statusText = "松江校区";//用于输出状态
}
else
{
statusText = "延安路校区";

}
// 输出状态
statusLabel.Text = "处理"+ statusText + "排课...";
//记录处理次数
int courseCount = 0;
//循环处理 松江校区排课的sheet
for (int j = 3; j <= 12; j++)//列号
{
//行号
for (int i = 1; i <= 40; i++) { 
//改进 
Range curentCell = (Range)tmpTable.Cells[i, j]; 
string tmpText = curentCell.Text.ToString().Trim(); 
//单元格文本 
// MessageBox.Show(i+" "+j+" "+tmpText); 
if (tmpText.Equals("")) { continue; } 
//将中文括号变成英文括号 
tmpText.Replace("(", "("); 
tmpText.Replace(")", ")"); 
//针对课程代码中的数据进行匹配 用了全匹配 可能比较慢---贼慢
 //解释在这里如果不懂啥意思 可以看看下面再上来 
// 发现 存的时候 是 王:男篮(高) 因此是不能直接相等 因此需要去掉前两个 
//再来一个临时字符 
string tmpCouse = ""; 
if (tmpText.Length >= 2)
{
    tmpText=tmpText.Replace(" ", "");
    tmpCouse =tmpText.Substring(2);
}
//  MessageBox.Show(tmpCouse);

for (int k = 0; k < this.courseCodeDs.Tables[0].Rows.Count; k++)
{
    //课程代码结构很明显  从 0 开始 0-序号 1 代表课程名称 2代表新生 3代表老生代码
    //[废弃]如果课程中包含了课程代码中的文字 那么进行匹配
    //[废弃]针对有些时候没有写男女  加了一次判定 本列第三行 是代表了男生女生
    //[废弃][第一次更新]第二次判断 如果里面没有写明男女 则判断课程代码中包含 比如 攀岩(男)【代码】 而课程中是 攀岩 列标题写了男 这样子也可以匹配
    //[废弃][第一次更新]上面的做法容易产生 男 女 单个字符的匹配 因此加了个判定不能只是 男 女
    //[第二次更新] 发现如果排课中 字数多 包含 字数少 不能正确匹配 比如 男篮(高)最后匹配的是 男篮。
    //[第二次更新] 只要一个条件 那就是相等!!就是强制让你一样,不服咬我啊! 判断是否完全相等
    // 日 le dog  看for循环上面的解释 ||
                                
    // 获取临时代码
    string tmpCourseCode = this.courseCodeDs.Tables[0].Rows[k][1].ToString();
    if (tmpCouse.Equals(tmpCourseCode))
    {
        //判断是新生还是老生
        //根据上面写死的 其中 当前行的第2列是表示大一还是大二、三的
        //因此根据 大一来判断新生否则是老生
        Range tmpGradeRange = (Range)tmpTable.Cells[i, 2];
        string tmpGrade = "";
        //判断是否是合并单元格
        if ((bool)tmpGradeRange.MergeCells)
        {
            Range mergeArea = (Range)tmpTable.Cells[tmpGradeRange.MergeArea.Row, tmpGradeRange.MergeArea.Column];
            tmpGrade = mergeArea.Text.ToString();
        } else
        {
            tmpGrade = tmpGradeRange.Text.ToString();
        }
        //把课程代码加上去  大一在课程代码中是新生 
        if (tmpGrade.Equals("大一"))
        {
            tmpText = tmpText + "      " + this.courseCodeDs.Tables[0].Rows[k][2];
        }
        else//老生 也就是 大二、大三、大四 不知道为什么老师没写大四 因此大一作为条件比较好
        {
            tmpText = tmpText + "      " + this.courseCodeDs.Tables[0].Rows[k][3];
        }
        //MessageBox.Show(tmpText);
        //修改新的单元格内容
        curentCell.Value=tmpText;
        curentCell.ColumnWidth = 25;
        // MessageBox.Show(curentCell.Text.ToString());
        courseCount++;
        //输出状态
        statusLabel.Text = "正在处理"+statusText+" 第 "+courseCount.ToString()+" 条数据...";
        break;
    }
    else
    {
        continue;
    }
}
}
}
//输出状态
statusLabel.Text = statusText+" 排课处理完成 ,共"+courseCount;
}
//保存文件
// _wbk.Save();
//退出
courseApp.AlertBeforeOverwriting = false;
_wbk.Close(null, null, null);
wbks.Close();
courseApp.Quit();
//释放掉多余的excel进程
System.Runtime.InteropServices.Marshal.ReleaseComObject(courseApp);
courseApp = null;
////输出状态
statusLabel.Text = "排课处理完成...Powered by postbird";
}
catch(Exception ex)
{
MessageBox.Show(ex.ToString(),"发生错误");
}

当然,各种连接的关闭还是要注意的。注意最后我还将Excel的进程给结束了。

附上软件截图和处理效果:

6

7

 

代码托管在git@osc和github:

git@osc: https://git.oschina.net/postbird/DHUCourseHelper

github: https://github.com/postbird/DHUCourseHelperPE