仅作自己学习适用
0 前言
在最近学习C#时,想要做一个和自己方向相关的东西巩固一下自己学的东西,上一篇博客写了一下动物识别系统,这是C#的基础语法。本篇想要巩固一下学习的winform课程,因次想到了这个孔隙度的计算软件。
1 技术
项目量很少,在熟练背景知识和winform基础的情况下只需几个小时就能完成,完成这个项目一共分为三层,表示层,业务逻辑层和数据层;需要完成的工作有:
- UI的涉及
- 自定义曲线的存储结构
- 标准txt数据格式的读和写
- 参数控制
- 绘图控制
- 孔隙度计算模型
2 软件截图
3 代码结构
页面布局用到的控件有:
label | textbox | button | groupBox | NumericalUpDown |
---|---|---|---|---|
ComboBox | Chart | TableLayoutPanel | DataGridView |
在完成对页面布局的涉及后,首先要完成的是对文件的读写操作,以方便后续的处理,因为读取的是曲线信息,因次要专门写一个曲线的类用于定义曲线的数据结构:
LogDataIO.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PORCalcApp
{
/// <summary>
/// 实现文件的读写
/// </summary>
public class LogDataIO
{
/// <summary>
/// 读取曲线
/// </summary>
/// <param name="FileName">文件路径</param>
/// <returns></returns>
public static List<LogCurve> ReadCurveList(string FileName)
{
// 曲线列表
List<LogCurve> curveList = new List<LogCurve>();
// 读取文件为一行一行的字符串数组(这种方式就不适用于特别大的文件,因为是一次性读取所有文件到一个数组里)
string[] strArray = System.IO.File.ReadAllLines(FileName);// 里边是按行按行存放的一组信息,就是txt里边的一行
// step1:判断所读取文件里有多少根曲线(nameStr里存放的就是曲线名字,多了一个深度,第一列都是深度信息)
// 可能不同文件里的拆分方式不同,把所有的情况要考虑到,第二个参数的意思是拆分的时候去除空
string[] nameStr = strArray[0].Split(" ,\t;".ToCharArray(),StringSplitOptions.RemoveEmptyEntries);
// step2:获取起始深度,终止深度,采样间隔
float topdep = float.Parse(strArray[1].Split(" ,\t;".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).First()); // 第二行的第一个数值为起始深度,仍然采用这种拆分方式
float botdep = float.Parse(strArray.Last().Split(" ,\t;".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).First()); // 最后一行的第一个数值为终止深度,仍然采用这种拆分方式
float spacing = (botdep - topdep)/(strArray.Length-1-1); //采样间隔 = (终止深度-起始深度)/(文件总行数-1-1);文件第一行是那些曲线名称,不是数据
// step3:曲线信息设置(从1开始循环,因为nameStr的第一个元素是深度,不是曲线)
for (int n = 1; n < nameStr.Length; n++)
{
LogCurve curve = new LogCurve();
curve.Name = nameStr[n];
curve.TopDep = topdep;
curve.BotDep = botdep;
curve.Spacing = spacing;
curve.Value = new float[strArray.Length-1]; //先给曲线的那组值初始化个数组用于存放
curveList.Add(curve);
}
// step4:获取曲线值(仍然是跳过第一行)
for (int n = 1; n < strArray.Length; n++)
{
// 取出当前行,先将当前行拆开
string[] strTempList = strArray[n].Split("\t, ;".ToCharArray(),StringSplitOptions.RemoveEmptyEntries);
for (int m = 0; m < curveList.Count; m++)
{
curveList[m].Value[n - 1] = float.Parse(strTempList[m + 1]);
}
}
return curveList;
}
/// <summary>
/// 将曲线写入到文件中
/// </summary>
/// <param name="FileName">目标文件名</param>
/// <param name="curveList">输出曲线列表</param>
public static void WriteCurveList(string FileName,List<LogCurve> curveList)
{
StringBuilder sb = new StringBuilder();
// 曲线深度
sb.Append("DEPTH".PadRight(16, ' ')); // 控制格式,向右填充空格到16个字符
// 将所有曲线名称添加进去
for (int i = 0; i < curveList.Count; i++)
{
sb.Append(curveList[i].Name.PadRight(16, ' '));
}
sb.AppendLine(); // 第一行写完后换行
// 添加曲线值
for (int n = 0; n < curveList.First().Value.Length; n++)
{
float dep = curveList.First().TopDep + n * curveList.First().Spacing; // 计算当前深度
sb.Append(dep.ToString().PadRight(16, ' '));
for(int m = 0;m<curveList.Count; m++) // 添加曲线值
{
sb.Append(curveList[m].Value[n].ToString("0.0000").PadRight(16, ' '));
}
sb.AppendLine();
}
// 写到文件中去
System.IO.File.WriteAllText(FileName, sb.ToString());
}
}
/// <summary>
/// 自定义的曲线的数据类型
/// </summary>
public class LogCurve
{
// 曲线名字
public string Name { get; set; }
// 单位
public string Unit { get; set; } = "";
// 起始深度
public float TopDep { get; set; } = 0;
// 终止深度
public float BotDep { get; set; } = 10000;
// 采样间隔
public float Spacing { get; set; } = 0.1f;
// 数据值
public float[] Value { get; set; }
}
}
第二步,完成对模型类的定义
Pars.cs
using System;
namespace PORCalcApp
{
/// <summary>
/// 模型类,参数相关信息
/// </summary>
public class Pars
{
// GR最小值
public float GRmin { get; set; } = 50.0f;
// GR最大值
public float GRmax { get; set; } = 250.0f;
// 地层参数
public float C { get; set; } = 4.0f;
// 密度最小值
public float DENmin { get; set; } = 1.2f;
// 密度最大值
public float DENmax { get; set; } = 2.9f;
// 泥质
public float DENclay { get; set; } = 1.7f;
/// <summary>
/// 计算泥质含量
/// </summary>
/// <param name="gr">伽马</param>
/// <param name="grmin">纯砂岩GR</param>
/// <param name="grmax">纯泥岩GR</param>
/// <param name="C">地层参数</param>
/// <returns></returns>
public static float CalVSH(float gr,float grmin,float grmax,float C)
{
float vsh = 0; //泥质含量
float igr = (gr - grmin) / (grmax - grmin); //gr体积因子
vsh = (float)((Math.Pow(2, C * igr) - 1) / ((Math.Pow(2, C) - 1)));
return vsh;
}
/// <summary>
/// 计算孔隙度
/// </summary>
/// <param name="den">密度</param>
/// <param name="denmin">流体密度</param>
/// <param name="denmax">骨架密度</param>
/// <param name="dencaly">泥质密度</param>
/// <param name="vsh">泥质含量</param>
/// <returns></returns>
public static float CalPOR(float den,float denmin,float denmax,float dencaly,float vsh)
{
float por = 0;
por = (den-denmax) /(denmin-denmax) - vsh*(dencaly - denmax)/(denmin-denmax);
return por;
}
}
}
第三步完成业务逻辑:
Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace PORCalcApp
{
public partial class Form1 : Form
{
private List<LogCurve> logCurves;
private LogCurve POR;
private Pars p;
public Form1()
{
InitializeComponent();
chart_GR.Series.Clear();
chart_DEN.Series.Clear();
chart_POR.Series.Clear();
InivializeControls();
}
/// <summary>
/// 给参数赋初值(初始化参数信息)
/// </summary>
private void InivializeControls()
{
p = new Pars();
numericUpDown_GRmax.Value = (decimal)p.GRmax;
numericUpDown_GRmin.Value = (decimal)p.GRmin;
numericUpDown_DENmax.Value = (decimal)p.DENmax;
numericUpDown_DENmin.Value = (decimal)p.DENmin;
numericUpDown_DENvsh.Value = (decimal)p.DENclay;
numericUpDown_RePar.Value = (decimal)p.C;
}
// 数据加载按钮
private void button_select_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "TEXT|*.txt"; //过滤一下可以选择的文件类型
if (ofd.ShowDialog() == DialogResult.OK)
{
textBox_file.Text = ofd.FileName;
logCurves = LogDataIO.ReadCurveList(ofd.FileName);
// 将曲线信息显示在表格里
Load2DataGridView();
// 数据加载完成后立即就有GR和DEN的曲线信息,将他们显示出来
Plot();
}
}
private void Plot()
{
绘制GR
// step1 先清空数据,放置产生累计效果
chart_GR.Series.Clear();
// step2 新建Serise存放数据
Series sgr = new Series();
sgr.ChartType = SeriesChartType.FastLine; // 指定类型为线性的
LogCurve lc = logCurves[comboBox_GR.SelectedIndex]; // 选中要画的曲线
// step3 绘图,将所有点绘制出来
for (int i = 0;i<lc.Value.Length;i++)
{
// 横坐标是曲线值,纵坐标是深度信息
sgr.Points.AddXY(lc.Value[i], lc.TopDep + i * lc.Spacing);
}
// step4 添加到图中去
chart_GR.Series.Add(sgr);
// step5 优化显示,否则图很丑
// 设置x轴的最大最小值(÷10再×10)
chart_GR.ChartAreas.First().AxisX.Minimum = ((int)lc.Value.Min() / 10) * 10;
chart_GR.ChartAreas.First().AxisX.Maximum = ((int)lc.Value.Max() / 10) * 10;
// 设置y轴的最大最小值
chart_GR.ChartAreas.First().AxisY.Minimum = ((int)lc.TopDep / 10) * 10;
chart_GR.ChartAreas.First().AxisY.Maximum = ((int)lc.BotDep / 10) * 10;
// 设置图例
sgr.Name = lc.Name;
绘制DEN
chart_DEN.Series.Clear();
Series sden = new Series();
sden.ChartType = SeriesChartType.FastLine;
lc = logCurves[comboBox_DEN.SelectedIndex]; // lc = logCurves[comboBox_DEN.SelectedIndex];
for (int i = 0; i < lc.Value.Length; i++)
{
sden.Points.AddXY(lc.Value[i], lc.TopDep + i * lc.Spacing);
}
chart_DEN.Series.Add(sden);
chart_DEN.ChartAreas.First().AxisX.Minimum = (float)lc.Value.Min();
chart_DEN.ChartAreas.First().AxisX.Maximum = (float)lc.Value.Max();
chart_DEN.ChartAreas.First().AxisY.Minimum = ((int)lc.TopDep / 10) * 10;
chart_DEN.ChartAreas.First().AxisY.Maximum = ((int)lc.BotDep / 10) * 10;
sden.Name = lc.Name;
绘制POR
if (POR == null) return;
Series spor = new Series();
spor.ChartType = SeriesChartType.FastLine;
lc = POR;
for (int i = 0; i < lc.Value.Length; i++)
{
spor.Points.AddXY(lc.Value[i], lc.TopDep + i * lc.Spacing);
}
chart_POR.Series.Add(spor);
chart_POR.ChartAreas.First().AxisX.Minimum = 0;
chart_POR.ChartAreas.First().AxisX.Maximum = ((int)lc.Value.Max()); //(int)lc.Value.Max()*10.0f + 10) * 10.0f
chart_POR.ChartAreas.First().AxisY.Minimum = ((int)lc.TopDep / 10) * 10;
chart_POR.ChartAreas.First().AxisY.Maximum = ((int)lc.BotDep / 10) * 10;
spor.Name = lc.Name;
}
private void Load2DataGridView()
{
// 简单方法,这样就不能修改了
// dataGridView_curvelist.DataSource = logCurves;
// 常规方法
dataGridView_curvelist.Rows.Clear(); //先将里边的内容清空
comboBox_DEN.Items.Clear();
comboBox_GR.Items.Clear();
for (int n = 0; n < logCurves.Count; n++)
{
// 添加一行
dataGridView_curvelist.Rows.Add();
// 给每行添加序号
dataGridView_curvelist.Rows[n].HeaderCell.Value = (n+1).ToString();
// 添加列信息
dataGridView_curvelist.Rows[n].Cells[0].Value = logCurves[n].Name; // 名字
dataGridView_curvelist.Rows[n].Cells[1].Value = logCurves[n].Unit; // 单位
dataGridView_curvelist.Rows[n].Cells[2].Value = logCurves[n].TopDep; // 起始深度
dataGridView_curvelist.Rows[n].Cells[3].Value = logCurves[n].BotDep; // 终止深度
dataGridView_curvelist.Rows[n].Cells[4].Value = logCurves[n].Spacing; // 采样间隔
comboBox_DEN.Items.Add(logCurves[n].Name); // 给两个下拉框添加上数据
comboBox_GR.Items.Add(logCurves[n].Name);
}
// 给DEN下拉框默认值
for (int n = 0; n < logCurves.Count; n++)
{
if (comboBox_DEN.Items[n].Equals("DEN"))
{
comboBox_DEN.SelectedIndex = n;
break;
}
}
// 如果没有找到目标DEN曲线
if (comboBox_DEN.SelectedIndex <= 0)
{
comboBox_DEN.SelectedIndex = 0;
}
// 给目标GR曲线默认值
for (int n = 0; n < logCurves.Count; n++)
{
if (comboBox_GR.Items[n].Equals("GR"))
{
comboBox_GR.SelectedIndex = n;
break;
}
}
// 如果没有找到目标GR曲线
if (comboBox_GR.SelectedIndex <= 0)
{
comboBox_GR.SelectedIndex = 0;
}
}
// 计算按钮
private void button_calc_Click(object sender, EventArgs e)
{
// step1 计算之前要把用户设置的参数保存起来
SavePars();
// step2 计算模型
RunMethod();
// step3 做出图像
Plot();
}
/// <summary>
/// 计算模型
/// </summary>
private void RunMethod()
{
POR = new LogCurve();
POR.Name = "POR";
POR.Unit = "%";
POR.TopDep = logCurves.First().TopDep;
POR.BotDep = logCurves.First().BotDep;
POR.Spacing = logCurves.First().Spacing;
POR.Value = new float[logCurves.First().Value.Length];
for (int n = 0; n < logCurves.First().Value.Length; n++)
{
// 取出GR和DEN
float gr = logCurves[comboBox_GR.SelectedIndex].Value[n];
float den = logCurves[comboBox_DEN.SelectedIndex].Value[n];
// 得到泥质含量
float vsh = Pars.CalVSH(gr,p.GRmin,p.GRmax,p.C);
// 得到孔隙度
POR.Value[n] = Pars.CalPOR(den, p.DENmin, p.DENmax, p.DENclay, vsh) * 100;
}
}
/// <summary>
/// 保存参数信息
/// </summary>
private void SavePars()
{
p.GRmax = (float)numericUpDown_GRmax.Value;
p.GRmin = (float)numericUpDown_GRmin.Value;
p.DENmax = (float)numericUpDown_DENmax.Value;
p.DENmin = (float)numericUpDown_DENmin.Value;
p.DENclay = (float)numericUpDown_DENvsh.Value;
p.C = (float)numericUpDown_RePar.Value;
}
// 保存按钮
private void button_save_Click(object sender, EventArgs e)
{
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "TEXT|*.txt";
if (sfd.ShowDialog() == DialogResult.OK)
{
// 将孔隙度加载进来
logCurves.Add(POR);
LogDataIO.WriteCurveList(sfd.FileName, logCurves);
MessageBox.Show("保存成功!");
}
}
// 清空按钮
private void button1_Click(object sender, EventArgs e)
{
chart_POR.Series.Clear();
}
}
}
4 总结
除了没有给出UI的代码,因为都是直接拖拽的,其余部分全部完成,完整的工程文件和打包好的安装程序:POR计算小软件.
这是自己学习时的总结,仅作自己学习使用。
tic: 2023.08.22
5 代码优化
仔细观察不难发现,在绘制GR,DEN,POR曲线时,很多地方都是重复的,因次可以将重复的部分提取出来,提高代码的复用,这里将三个绘图的地方提取为一个PlotChart()方法:
void PlotChart(Chart chart,LogCurve lc,SeriesChartType sType,double AxisXMin,double AxisXMax)
{
chart.Series.Clear();
Series s = new Series();
s.ChartType = sType; // 指定绘制类型
for (int i = 0; i < lc.Value.Length; i++)
{
double x = lc.Value[i];
double y = lc.TopDep + i * lc.Spacing;
s.Points.AddXY(x, y);
}
chart.ChartAreas.First().AxisX.Minimum = AxisXMin;
chart.ChartAreas.First().AxisX.Maximum = AxisXMax; //Math.Round(lc.Value.Max()+2 , 2)
chart.ChartAreas.First().AxisY.Minimum = lc.TopDep / 10 * 10;
chart.ChartAreas.First().AxisY.Maximum = lc.BotDep / 10 * 10;
chart.Series.Add(s);
}
这样在三个函数那里调用就可以了,如下:
// 绘制GR
LogCurve log = logCurves[comboBox_GR.SelectedIndex];
double AxisXMin = Math.Round(log.Value.Min() - 2, 2);
double AxisXMax = Math.Round(log.Value.Max() + 2, 2);
PlotChart(chart_GR, log, SeriesChartType.Line, AxisXMin, AxisXMax);
// 绘制DEN
log = logCurves[comboBox_DEN.SelectedIndex];
AxisXMin = Math.Round(log.Value.Min()-0.1 , 3);
AxisXMax = Math.Round(log.Value.Max()+0.1 , 3);
PlotChart(chart_DEN, log, SeriesChartType.Line, AxisXMin, AxisXMax);
// 绘制孔隙度
if (POR == null) return;
AxisXMin = 0;
AxisXMax = Math.Round(POR.Value.Max() + 0.1, 3);
PlotChart(chart_POR, POR, SeriesChartType.FastLine, AxisXMin, AxisXMax);
TODO:
这里没有指定对y轴刻度(深度方向)的最大最小值的调整,因为我认为三条曲线的深度信息是相同的,因次没有调整(不知道理解是否正确)。
tic:2023.08.27