本文系原创,转载请注明出处:
https://blog.youkuaiyun.com/chengbao315/article/details/82874729
在之前分享的博客中,我已经实现了一个静态加载的小框架,这个框架的模块已经在代码中确定,一旦生成程序,模块将无法改变。但在实际应用的大型项目中,我们更倾向于使用动态加载模块的框架,这样对于项目的移植更加灵活和方便,因此今天我就来实现这个效果,和大家一起分享。先看结果展示:
大家看这个动图是不是有种眼花缭乱的赶脚,没办法,优快云只让上传5M的图片。这次的案例会用到数据库设计、数据库编程、动图加载控件、反射、系统ListView控件等等知识,实现比较复杂,所以我打算分两部分来分享这个小框架的实现过程。
一、数据库设计
因为项目比较简单,并且为了便于使用,本案例中我使用了SQLite数据库。在写代码之前大家需要自行下载 System.Data.SQLite.dll 动态库和 sqlitestudio 数据库管理软件,这里就不做介绍了,如果找不到资源,可以给我留言。
数据库的设计也比较简单,我只添加了几个必要字段,主键、模块名称、动态库路径、类名称、使能、排序、时间戳、图标,如图所示:
以上设计就可以满足我要实现的功能了,如有需要,大家可以自行扩展。接下来需要准备DBHelper.cs,网上版本也是一大堆,我这里贴出一版供参考,看代码:
public class SQLiteHelper
{
private static string dbFile = "myFrame.db";
//生成连接字符串
private static string CreateConnectionString()
{
SQLiteConnectionStringBuilder connectionString = new SQLiteConnectionStringBuilder();
connectionString.DataSource = @"Data/" + dbFile;
string conStr = connectionString.ToString();
return conStr;
}
/// <summary>
/// 对插入到数据库中的空值进行处理
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static object ToDbValue(object value)
{
if (value == null)
{
return DBNull.Value;
}
else
{
return value;
}
}
/// <summary>
/// 对从数据库中读取的空值进行处理
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static object FromDbValue(object value)
{
if (value == DBNull.Value)
{
return null;
}
else
{
return value;
}
}
/// <summary>
/// 执行非查询的数据库操作
/// </summary>
/// <param name="sqlString">要执行的sql语句</param>
/// <param name="parameters">参数列表</param>
/// <returns>返回受影响的条数</returns>
public static int ExecuteNonQuery(string sqlString, params SQLiteParameter[] parameters)
{
string connectionString = CreateConnectionString();
using (SQLiteConnection conn = new SQLiteConnection(connectionString))
{
conn.Open();
using (SQLiteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = sqlString;
foreach (SQLiteParameter parameter in parameters)
{
cmd.Parameters.Add(parameter);
}
return cmd.ExecuteNonQuery();
}
}
}
/// <summary>
/// 执行查询并返回查询结果第一行第一列
/// </summary>
/// <param name="sqlString">SQL语句</param>
/// <param name="sqlparams">参数列表</param>
/// <returns></returns>
public static object ExecuteScalar(string sqlString, params SQLiteParameter[] parameters)
{
string connectionString = CreateConnectionString();
using (SQLiteConnection conn = new SQLiteConnection(connectionString))
{
conn.Open();
using (SQLiteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = sqlString;
foreach (SQLiteParameter parameter in parameters)
{
cmd.Parameters.Add(parameter);
}
return cmd.ExecuteScalar();
}
}
}
/// <summary>
/// 查询多条数据
/// </summary>
/// <param name="sqlString">SQL语句</param>
/// <param name="parameters">参数列表</param>
/// <returns>返回查询的数据表</returns>
public static DataTable GetDataTable(string sqlString, params SQLiteParameter[] parameters)
{
string connectionString = CreateConnectionString();
using (SQLiteConnection conn = new SQLiteConnection(connectionString))
{
conn.Open();
using (SQLiteCommand cmd = conn.CreateCommand())
{
cmd.CommandText = sqlString;
foreach (SQLiteParameter parameter in parameters)
{
cmd.Parameters.Add(parameter);
}
DataSet ds = new DataSet();
SQLiteDataAdapter adapter = new SQLiteDataAdapter(cmd);
adapter.Fill(ds);
return ds.Tables[0];
}
}
}
}
好了,以上我们的数据库准备就基本OK了,下面看模块动态库的准备。
二、准备加载模块
看过前两期文章的朋友应该知道,我之前的模块是与主程序放在一个项目中的,但实际应用中通常模块是分开的,这里我需要新建一个动态库项目,将之前的代码文件添加进去即可。没有做过之前项目的朋友也一样,在新建的动态库项目中新建几个模块界面即可。
如图所示,ModuleLib 就是我的模块动态库项目,里面添加了3个模块,大家根据自己需要设计。
三、设计配置页
先来看设计效果吧:
界面设计也非常简单一个显示的Table,三个增、删、改按钮和两个确认、取消按钮就OK了,关于这个Table,有很多第三方的控件可以支持,我用过的有FarPoint、Dev GridControl等等,非常方便,大家也可以自行设计。这里为了简单实现,尽量不要其他工具,我使用了系统自带的 ListView 控件。
使用 ListView 控件需要配置以下几个属性:
1. Columns(表头)
2. View (设置成Details样式)
3. GridLines(表格线)
设置完这些,我们的界面设计就基本OK了,其他按钮添加背景图片和文字就可以了。在这个配置页点击新增和编辑按钮都会弹出一个编辑页,下面需要设计一下这个界面。
四、设计编辑页
先看效果图:
这个界面就更简单了,只需要几个文本框、下拉框、按钮即可,这里就不做讲解了。强调一下就是下拉框的 DropDownStyle 属性要设置成 DropDownList,不允许编辑的状态。
以上我完成了界面的设计部分,下面我接着来讲具体的代码实现。先来看模块配置页面的实现,看代码:
private void LoadItem()
{
string sql = "select * from Frame order by SortNum";
DataTable dt = SQLiteHelper.GetDataTable(sql);
for (int i = 0; i < dt.Rows.Count;i++ )
{
ModuleClass Module = new ModuleClass();
Module.ModuleId = dt.Rows[i]["ModuleId"].ToString();
Module.ModuleName = dt.Rows[i]["ModuleName"].ToString();
Module.LibPath = dt.Rows[i]["LibraryPath"].ToString();
Module.Moduleclass = dt.Rows[i]["ModuleClass"].ToString();
Module.SortNum = int.Parse(dt.Rows[i]["SortNum"].ToString());
Module.Enable = dt.Rows[i]["Enable"].ToString() == "True";
Module.Image = dt.Rows[i]["img"].ToString();
this.AddItem(Module);
}
}
首先页面加载进来时需要读取数据库,读取现在已经存在的模块数据,这里我用一个方法实现,直接编写sql ,调用dbhelper的查询方法即可。 同时我习惯采用的方式是实体编程,将查询到的结果赋值给一个实体,然后对实体进行操作。当然,你也可以直接控件赋值或者声明一个table结构赋值,看个人喜好。界面的操作代码如下:
private void AddItem(ModuleClass module)
{
System.Windows.Forms.ListViewItem listViewItem = new System.Windows.Forms.ListViewItem(new string[] {
module.ModuleName,
module.Enable.ToString(),
module.LibPath,
module.Moduleclass,
module.SortNum.ToString()}, int.Parse(module.Image));
if (module.ModuleId != null)
{
listViewItem.Tag = module.ModuleId;
}
else
{
listViewItem.Tag = module;
}
this.listView1.Items.Add(listViewItem);
}
ListView控件添加一个Item,Item里是需要显示的数据,这里注意一下,我将实体赋给了Item的Tag 属性,这里用于后面编辑时获取选择项目所用,就不需要重新从ListView界面取值了。之后给按钮添加点击事件,添加和编辑按钮事件处理函数中弹出编辑页,而删除按钮事件处理函数中删除数据即可,这里不做解释,下面来看编辑页代码:
public FrmEditModule()
{
InitializeComponent();
for (int i = 0; i < Resources.ImageList.Images.Count; i++)
{
this.cbxImg.Items.Add(i.ToString());
}
}
public FrmEditModule(ModuleClass m)
{
InitializeComponent();
for (int i = 0; i < Resources.ImageList.Images.Count; i++)
{
this.cbxImg.Items.Add(i.ToString());
}
this.tbxName.Text = m.ModuleName;
this.tbxLib.Text = m.LibPath;
//加载程序集(dll文件地址),使用Assembly类
Assembly assembly = Assembly.LoadFile(this.tbxLib.Text);
//获取类型,参数(名称空间+类)
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (type.BaseType.Name == "UserControl")
{
this.cbxClass.Items.Add(type.Name);
}
}
this.cbxClass.Text = m.Moduleclass;
this.tbxNum.Text = m.SortNum.ToString();
this.cbxEnable.Text = "true";
this.cbxImg.SelectedIndex = int.Parse(m.Image);
}
编辑页中我设计了两个构造函数,不带参数的用于添加控件时使用,如果不需要显示图标,构造函数中可以什么都不做。第二个带有实体参数的构造函数用于编辑控件时使用,对界面进行显示赋值。可以看到第二个构造函数中运用了反射的方式获取模块类。
剩下的控件事件就比较简单了,直接看代码不解释:
// 确认按钮事件
private void btnOk_Click(object sender, EventArgs e)
{
this.Module = new ModuleClass();
if (this.tbxName.Tag != null)
{
Module.ModuleId = this.tbxName.Tag.ToString();
}
Module.ModuleName = this.tbxName.Text;
Module.LibPath = this.tbxLib.Text;
Module.Moduleclass = this.cbxClass.Text;
Module.SortNum = int.Parse(this.tbxNum.Text);
Module.Enable = bool.Parse(this.cbxEnable.Text);
Module.Image = this.cbxImg.SelectedIndex.ToString();
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
// 取消按钮事件
private void btnCancel_Click(object sender, EventArgs e)
{
this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
}
// 选择动态库按钮点击事件
private void btnselect_Click(object sender, EventArgs e)
{
OpenFileDialog diag = new OpenFileDialog();
diag.Filter = "dll文件|*.dll";
if (diag.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
this.tbxLib.Text = diag.FileName;
//加载程序集(dll文件地址),使用Assembly类
Assembly assembly = Assembly.LoadFile(this.tbxLib.Text);
//获取类型,参数(名称空间+类)
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (type.BaseType.Name == "UserControl")
{
this.cbxClass.Items.Add(type.Name);
}
}
}
}
这里我想着重介绍一下图标选择框的处理方法,正常的comboBox 控件中是没有图片只显示文本的,但是这里做成了图片和文字同时下拉显示的效果,comboBox 控件需要注册一个DrawItem 事件,在事件处理函数中进行重绘操作,代码如下:
private void comboBox1_DrawItem(object sender, DrawItemEventArgs e)
{
Graphics g = e.Graphics;
Rectangle r = e.Bounds;
Size imageSize = Resources.ImageList.ImageSize;
Font fn = null;
if (e.Index >= 0)
{
fn = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
string s = (string)cbxImg.Items[e.Index];
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Near;
if (e.State == (DrawItemState.NoAccelerator | DrawItemState.NoFocusRect))
{
//画条目背景
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(0, 64, 64)), r);
//绘制图像
Resources.ImageList.Draw(e.Graphics, r.Left, r.Top, e.Index);
//显示字符串
e.Graphics.DrawString(s, fn, new SolidBrush(Color.Black), r.Left + imageSize.Width, r.Top);
//显示取得焦点时的虚线框
e.DrawFocusRectangle();
}
else
{
e.Graphics.FillRectangle(new SolidBrush(Color.LightBlue), r);
Resources.ImageList.Draw(e.Graphics, r.Left, r.Top, e.Index);
e.Graphics.DrawString(s, fn, new SolidBrush(Color.Black), r.Left + imageSize.Width, r.Top);
e.DrawFocusRectangle();
}
}
}
最后回到主页面,主界面需要添加一个动态加载按钮和module的方法。我的思路是直接从数据库读取module 信息,根据module 数量决定添加按钮的数目,并通过反射获取module 对应的实体控件在主界面显示出来,代码如下
private void LoadModule() { this.panel3.Controls.Clear(); string sql = "select * from Frame where Enable = 1 order by SortNum"; DataTable dt = SQLiteHelper.GetDataTable(sql); for (int i = 0; i < dt.Rows.Count; i++) { ModuleClass Module = new ModuleClass(); Module.ModuleId = dt.Rows[i]["ModuleId"].ToString(); Module.ModuleName = dt.Rows[i]["ModuleName"].ToString(); Module.LibPath = dt.Rows[i]["LibraryPath"].ToString(); Module.Moduleclass = dt.Rows[i]["ModuleClass"].ToString(); Assembly assembly = Assembly.LoadFile(Module.LibPath); //获取类型,参数(名称空间+类) Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (type.Name == Module.Moduleclass) { Module.Moduleclass = type.FullName; } } myFrameModule.AppleKey btn = new myFrameModule.AppleKey(); btn.BackColor = System.Drawing.Color.Transparent; btn.BorderColor = System.Drawing.Color.FromArgb(0, 254, 252); btn.FlatAppearance.BorderSize = 0; btn.FlatStyle = System.Windows.Forms.FlatStyle.Flat; btn.ForeColor = System.Drawing.Color.Cyan; btn.Location = new System.Drawing.Point(45 + i *100, 23); btn.Name = dt.Rows[i]["ModuleId"].ToString(); btn.Size = new System.Drawing.Size(80, 33); btn.Text = dt.Rows[i]["ModuleName"].ToString(); btn.Type = myFrameModule.AppleKeyType.Button; btn.Click += new EventHandler(btn_Click); btn.Tag = Module; this.panel3.Controls.Add(btn); } } 讲到这里我们的动态加载框架就基本实现了,这是我最近研究出来的一点小成果,欢迎各路朋友批评指教,如有疑问,欢迎留言,再次感谢大家支持!