2023年2月

在上篇《
Socket开发探秘--基类及公共类的定义
》中介绍过,所有受到的数据包,经过系统的预处理后,都会得到一个PreData的数据实体,该实体包含了协议头、协议内容和所属用户的ID。PreData是定义了一个标准的协议数据格式,包含了协议关键字、协议内容、用户标识的内容。
前面说了,我们数据是通过实体类作为载体的,我们知道,收到的Socket数据经过粗略的解析后,就是PreData类型的数据,这个是通用的数据格式,我们需要进一步处理才能转化为所能认识的数据对象(实体类对象),同样,我们发送数据的时候,内容部分肯定是按照一定协议规则串联起来的数据,那么我们就需要把实体转化为发送的数据格式。综上所述,我们通过实体类,必须实现数据的发送和读取的转换。



代码


///

<summary>


///
测试数据的实体类信息

///

</summary>



public

class
TestDataRequest
{

#region
MyRegion



///

<summary>


///
请求序列

///

</summary>



public

string
seq;

///

<summary>


///
用户帐号

///

</summary>



public

string
userid;

///

<summary>


///
用户密码

///

</summary>



public

string
psw;


#endregion



public
TestDataRequest(
string
seq,
string
userid,
string
psw)
{

this
.seq
=
seq;

this
.userid
=
userid;

this
.psw
=
psw;
}

public
TestDataRequest()
{
}


///

<summary>


///
转换Socket接收到的信息为对象信息

///

</summary>


///

<param name="data">
Socket接收到的信息
</param>



public
TestDataRequest(
string
data)
{

string
[] dataArray
=

null
;
dataArray

=
NetStringUtil.UnPack(data);

if
(dataArray
!=

null

&&
dataArray.Length
>

0
)
{
TestDataRequest newAnswerData

=

new
TestDataRequest();

int
i
=

0
;

this
.seq
=
dataArray[i
++
];

this
.userid
=
dataArray[i
++
];

this
.psw
=
dataArray[i
++
];
}
}


///

<summary>


///
转换对象为Socket发送格式的字符串

///

</summary>


///

<returns></returns>



public

override

string
ToString()
{

string
data
=

""
;
data

=

this
.seq
+

"
|
"

+

this
.userid
+

"
|
"

+

this
.psw.ToString();
data

=
NetStringUtil.PackSend(DataTypeKey.TestDataRequest, data);

return
data;
}

以上把数据的处理放在了实体类中进行封包和拆包,是一种比较好的做法,但是由于数据的封包拆包是一个繁琐的过程,代码重复性比较多,而且也容易出错。

这里设计了一个基类,来改进这种方式的数据处理,我们把所有对数据的拆包和封包,利用反射机制,减少我们的代码量,提高代码的优雅性。



代码


public

class
BaseEntity
{

protected

string
HeaderKey;


public
BaseEntity()
{
}


///

<summary>


///
转换Socket接收到的信息为对象信息

///

</summary>


///

<param name="data">
Socket接收到的信息
</param>



public
BaseEntity(
string
data)
{

string
[] dataArray
=

null
;
dataArray

=
NetStringUtil.UnPack(data);

if
(dataArray
!=

null

&&
dataArray.Length
>

0
)
{

int
i
=

0
;
FieldInfo[] fieldArray

=
ReflectionUtil.GetFields(
this
);

if
(fieldArray
==

null

||
dataArray.Length
!=
fieldArray.Length)
{

throw

new
ArgumentException(
"
收到的信息和字段信息不一致
"
);
}


if
(fieldArray
!=

null
)
{

foreach
(FieldInfo info
in
fieldArray)
{

string
strValue
=
dataArray[i
++
];
ReflectionUtil.SetField(

this
, info.Name, strValue);
}
}
}
}


///

<summary>


///
转换对象为Socket发送格式的字符串

///

</summary>


///

<returns></returns>



public

override

string
ToString()
{

string
data
=

""
;
FieldInfo[] fieldArray

=
ReflectionUtil.GetFields(
this
);
StringBuilder sb

=

new
StringBuilder();

if
(fieldArray
!=

null
)
{

foreach
(FieldInfo info
in
fieldArray)
{
sb.Append(ReflectionUtil.GetField(

this
, info.Name));
sb.Append(

"
|
"
);
}
}

data

=
sb.ToString().Trim(
'
|
'
);

if
(
string
.IsNullOrEmpty(HeaderKey))
{

throw

new
ArgumentNullException(
"
DataTypeKey
"
,
"
实体类未指定协议类型
"
);
}
data

=
NetStringUtil.PackSend(HeaderKey, data);

return
data;
}
}

以上的是实体类的基类,它封装了数据的拆包和封包过程,只需要在子类代码中指定协议头就可以了。子类的代码如下所示。



代码


///

<summary>


///
测试请求

///

</summary>



public

class
TestDataRequest : BaseEntity
{

#region
字段信息



///

<summary>


///
请求序列

///

</summary>



public

string
Seq;


///

<summary>


///
用户帐号

///

</summary>



public

string
UserId;


///

<summary>


///
用户密码

///

</summary>



public

string
Password;


///

<summary>


///
消息时间

///

</summary>



public
DateTime CreateDate
=
DateTime.Now;


#endregion



public
TestDataRequest()
{

this
.HeaderKey
=
DataTypeKey.TestDataRequest;
}


public
TestDataRequest(
string
seq,
string
userid,
string
psw)
{

this
.Seq
=
seq;

this
.UserId
=
userid;

this
.Password
=
psw;

this
.HeaderKey
=
DataTypeKey.TestDataRequest;
}


///

<summary>


///
转换Socket接收到的信息为对象信息

///

</summary>


///

<param name="data">
Socket接收到的信息
</param>



public
TestDataRequest(
string
data) :
base
(data)
{

this
.HeaderKey
=
DataTypeKey.TestDataRequest;
}
}

下面的代码是收到数据包,利用实体类构造函数,解析为实体类的操作,以及构造实体类,通过ToString()方式把实体类信息转化为可以发送的数据包的操作。



代码


private

void
TestDataHandle(PreData data)
{
TestDataRequest request

=

new
TestDataRequest(data.Content);
Log.WriteInfo(

string
.Format(
"
############{0}
"
, request.ToString()));

TestDataAnswerData answerData

=

new
TestDataAnswerData(request.Seq, request.UserId, request.Password);
ShopClientManager.This.AddSend(data.UserId, answerData.ToString(),

true
);
}

我编写的测试例子中,实体类的继承图如下所示。

报表模块几乎是各种大小管理系统都是必不可少的一个模块,而往往报表都需要有数据查看、打印、导出、数据汇总等方面,原本我在准备做酒店管理系统的时候,曾经考虑过试用ActiveReport报表控件的,因为我以前的送水管理系统就是采用这个来做报表的,因此曾经写过一篇文章《
ActiveReport报表开发---谈谈ActiveReport的中文化问题
》,提前为做中文报表做准备。

做ActiveReport的报表,大致需要下面几个步骤,设计好报表表现内容的文件,设计一个通用的窗体用来实现Preview功能的报表查看窗体,再设计一个通用的报表函数进行统一调用,如下所示:



代码


///

<summary>


///
提供通用的打印函数

///

</summary>


///

<param name="rpt1"></param>


///

<param name="ds"></param>



public

void
PrintPreview(ActiveReport3 rpt1,
ref
DataSet ds)
{

try

{
FrmPrintPreview frm

=

new
FrmPrintPreview();
frm.StartPosition

=
FormStartPosition.CenterScreen;

//
frm.Dock = DockStyle.Fill;

//
frm.WindowState = FormWindowState.Maximized;


frm.Show();
rpt1.DataSource

=
ds.Tables[
0
];

//
rpt1.DataMember = cmbQuery.Text;


rpt1.Run();
frm.arvMain.Document

=
rpt1.Document;
}

catch
(Exception ex)
{
MessageUtil.ShowError(ex.Message);
}
}

这样通过报表文件和界面的数据源,就可以实现报表的显示了。

不过由于这种方式,必须为每个报表都设计一个报表文件,如下所示。

最后得到的报表界面大致如下所示。

使用这个ActiveReport来实现我的报表功能的话,这样如果我的报表非常多,那么这个工作量就比较吓人了,最后还是放弃了这种方式,采用了改造我的分页控件的方式来实现,既可以方便数据的展示,有可以继承了报表预览打印、导出等功能,而且这样做的好处就是,我省却了不用设计那么多报表格式文件的时间,并且总体效果也非常不错。

在我前面的文章有介绍过Winform分页控件的内容《
WinForm界面开发之“分页控件”
》,该控件集成了分页、导出Excel、打印、右键菜单等操作,我唯一需要改造的主要就是不用分页功能,然后在增加一些细微的修改就可以了。先看看我的酒店管理系统中的一些报表界面吧。




总体上就是我们一般报表所需要的功能。其中报表打印预览可以设置报表标题,打印的列也可以设定,有一些字段的汇总功能,而且这样的报表基本上不需要额外的代码就能实现(相对分页控件来说)。

下面我们来看看我这个控件大致的代码实现。

首先在Form窗体初始化的时候,指定该控件所需要的一些载体,如空的菜单控件、空的Progressbar进度条控件。



代码


private

void
KFTopTradeReport_Load(
object
sender, EventArgs e)
{
InitDictItem();


this
.winGridView1.ProgressBar
=

this
.toolStripProgressBar1.ProgressBar;

this
.winGridView1.AppendedMenu
=

this
.contextMenuStrip1;


this
.dtStart.Value
=
Convert.ToDateTime(DateTime.Now.ToString(
"
yyyy-MM-dd 00:00:00
"
));

this
.dtEnd.Value
=
Convert.ToDateTime(DateTime.Now.ToString(
"
yyyy-MM-dd 23:59:59
"
));

}

然后在所需要的地方,如按钮、更新操作,调用下面的函数就可以了。



代码


private

void
BindItemCateData()
{

#region
添加别名解析


this
.winGridView1.AddColumnAlias(
"
Rank
"
,
"
名次
"
);

this
.winGridView1.AddColumnAlias(
"
ItemName
"
,
"
项目名称
"
);

this
.winGridView1.AddColumnAlias(
"
Price
"
,
"
商品单价
"
);

this
.winGridView1.AddColumnAlias(
"
Quantity
"
,
"
销售数量
"
);

this
.winGridView1.AddColumnAlias(
"
OriginalAmount
"
,
"
销售总额
"
);

this
.winGridView1.AddColumnAlias(
"
OfferMoney
"
,
"
优惠金额
"
);

this
.winGridView1.AddColumnAlias(
"
Amount
"
,
"
折后金额
"
);

#endregion



#region
条件检索


SearchCondition condition

=

new
SearchCondition();
condition.AddCondition(

"
ItemType
"
,
this
.txtStatisticItem.Text, SqlOperator.Like)
.AddCondition(

"
BeginTime
"
,
this
.dtStart.Value.ToString(), SqlOperator.MoreThanOrEqual)
.AddCondition(

"
BeginTime
"
,
this
.dtEnd.Value.ToString(), SqlOperator.LessThanOrEqual);

string
filter
=
condition.BuildConditionSql();


int
topCount
=
Convert.ToInt32(
this
.txtTopCount.Text);

#endregion


DataTable dt

=
BLLFactory
<
ConsumerList
>
.Instance.GetTopTradeReport(topCount, filter);

int
i
=

1
;

foreach
(DataRow row
in
dt.Rows)
{
row[

"
Rank
"
]
=
i
++
;
//
修改名次信息


}

#region
增加汇总信息


if
(dt.Rows.Count
>

0
)
{
DataRow dr

=
dt.NewRow();
dr[

"
ItemName
"
]
=

string
.Format(
"
项目数:{0}
"
, dt.Rows.Count);

decimal
Price
=
0M;

decimal
Quantity
=
0M;

decimal
OriginalAmount
=
0M;

decimal
OfferMoney
=
0M;

decimal
Amount
=
0M;

foreach
(DataRow row
in
dt.Rows)
{
Price

+=
Convert.ToDecimal(row[
"
Price
"
]);
Quantity

+=
Convert.ToDecimal(row[
"
Quantity
"
]);
OriginalAmount

+=
Convert.ToDecimal(row[
"
OriginalAmount
"
]);
OfferMoney

+=
Convert.ToDecimal(row[
"
OfferMoney
"
]);
Amount

+=
Convert.ToDecimal(row[
"
Amount
"
]);
}
dr[

"
Price
"
]
=
Price;
dr[

"
Quantity
"
]
=
Quantity;
dr[

"
OriginalAmount
"
]
=
OriginalAmount;
dr[

"
OfferMoney
"
]
=
OfferMoney;
dr[

"
Amount
"
]
=
Amount;

dt.Rows.Add(dt.NewRow());
dt.Rows.Add(dt.NewRow());
dt.Rows.Add(dr);
}

#endregion


this
.winGridView1.DataSource
=
dt.DefaultView;

this
.winGridView1.PrintTitle
=
Portal.gc.gAppUnit
+

"
--
"

+

"
客房按项目类别统计报表
"
;
}


以上代码,和分页控件一样,就是对显示的字段进行中文的转义显示,构造查询条件并检索数据,汇总报表内容,然后绑定到自定义控件上,就可以了。

其中检索的代码大致如下所示就可以了,返回的DataTable,当然,如果你的数据源是实体类集合,如List<MyInfo>()的格式数据源,一样正常显示的。



代码


///

<summary>


///
获取贸易排行报表

///

</summary>


///

<param name="condition"></param>


///

<returns></returns>



public
DataTable GetTopTradeReport(
int
topCount,
string
condition)
{

string
sql
=

string
.Format(
@"
select top {0} '0' as Rank, ItemName, Price,
sum(Quantity) Quantity,sum(Price*Quantity) OriginalAmount,sum((Price-DiscountPrice)*Quantity) OfferMoney,sum(Amount) Amount
from KF_ConsumerList {1} And ItemName <> '部分结账'
group by ItemName,Price order by Sum(Quantity) DESC

"
, topCount, condition);


return

base
.SqlTable(sql);
}

我们说看到的就是,基本上所有的报表展示,都不需要关注报表预览的问题,只是关注如何在GridView控件中显示就可以了,因为打印和导出,都是集中管理的。

最后,呈上我所封装的控件,大家可以直接过来使用就可以了,没有任何限制。

分页控件、报表显示控件:
https://files.cnblogs.com/wuhuacong/WinformControl.rar

如果你有好的建议或想法,欢迎大家进行沟通交流。

选择优于输入,这是一般人的共识,面对繁多的数据,提供良好的选择界面,一方面增强用户的界面体验,一方面也提高了数据的准确性,更节省了用户的宝贵时间。一般的单项数据选择可以使用
DropdownList
控件来实现,但对于有多个选择性输入,而且输入有层次关系的内容,最好选择
TreeView
控件来实现。


本文介绍如何使用使用
TreeView
控件来有效获取用户的输入,其中涉及到
TreeView
控件的级联选择、去掉节点
HTML
链接变为展开目录、获取选择内容、如何构造数据库的信息变为树形内容以及弹出窗口使用等知识点,本文输入应用级别的例子,希望能做个记号,对己对人,皆为利好
!^_^

本文的经营范围是一个可以输入分类及详细子内容的,由于内容繁多,而且具有一定的层次关系,因此,不适合采用
DropdownList

CheckboxList
控件,因此采用了带
CheckBox
属性的
TreeView
控件来辅助用户的输入。

输入界面大致如下所示,用户通过选择按钮,触发弹出对话框,在对话框中放置了
TreeView
控件。






































在弹出的对话框中,放置的
TreeView
控件,一个带有
CheckBox
,可以方便用户选择,并且具有级联(通过
Javascript
实现,减少
Post
回发),另外由于内容比较多,我设置了展开的级别层次。

用户通过选择或者反选大类,可以选择或反选其列表下面的所有项目,也可以单独选择子项目。

由于通过Javascript不太好获取并组装返回的内容,本文通过了在后台遍历树的方式对返回值进行处理,然后在父窗体的Javascript中对返回值进行了绑定,使其在界面控件中得以显示指定格式的内容。


以下为
HTML
的代码,其中
OnTreeNodeChecked
为级联
Javascript
函数,
SubmitValue
为对返回值进行绑定的操作。






代码


<
div
class
="search">


<
span
>


<
asp:ImageButton
ID
="btnSelect"
runat
="server"

ImageUrl

="~/Themes/Default/btn_select.gif"
onclick
="btnSelect_Click"


/>


<
asp:ImageButton
ID
="btnClose"
runat
="server"
OnClientClick
="javascript:window.close();return false;"

ImageUrl

="~/Themes/Default/btn_close.gif"

/>


</
span
>


<
table
cellspacing
="0"
cellpadding
="0"
border
="0"
width
="100%">


<
tr
>


<
td
class
="ico">


&nbsp;


</
td
>


<
td
class
="form">


<
asp:TreeView
ID
="TreeView1"
runat
="server"
onclick
="OnTreeNodeChecked();"
ShowCheckBoxes
="All"

ShowLines

="True"
ExpandDepth
="1"
Font-Bold
="False"
ForeColor
="#0000CC">


</
asp:TreeView
>


</
td
>


</
tr
>


</
table
>


</
div
>



<
script
language
='javascript'
type
='text/javascript'>


function
OnTreeNodeChecked() {

var
ele = event.srcElement;

if
(ele.type == 'checkbox') {

var
childrenDivID = ele.id.replace('CheckBox', 'Nodes');

var
div = document.getElementById(childrenDivID);

if
(div ==
null
)
return
;

var
checkBoxs = div.getElementsByTagName('INPUT');

for
(
var
i = 0; i < checkBoxs.length; i++) {

if
(checkBoxs[i].type == 'checkbox')
checkBoxs[i].checked = ele.checked;
}
}
}


function
SubmitValue() {

var
val = "";

var
returnVal =
new
Array();

var
inputs = document.all.tags("INPUT");

var
n = 0;

for
(
var
i = 0; i < inputs.length; i++)
//

遍历页面上所有的
input

{

if
(inputs[i].type == "checkbox") {

if
(inputs[i].checked) {

var
strValue = inputs[i].value;
val += strValue + ',';

//returnVal[n] = val;

n = n + 1;
}
}

//if(inputs[i].type="checkbox")

}
//for


window.returnValue = val;
window.close();
}


</
script
>



下面代码是页面的后台代码,其中展示了如何对树进行数据绑定,使其能够显示有层次格式的内容,其中
AddTreeNode
是一个递归函数。
btnSelect_Click
事件处理函数,专门对返回的数据进行组装,以一定的格式显示到客户端的控件输入上。






代码


protected

void
Page_Load(
object
sender, EventArgs e)
{

if
(!
this
.IsPostBack)
{
BindData();
}
}


private

void
BindData()
{
ArrayList scopeTree = BLLFactory<BusinessScope>.Instance.GetTree();

foreach
(BusinessScopeNodeInfo nodeInfo
in
scopeTree)
{
TreeNode node =

new
TreeNode(nodeInfo.Name);
node.SelectAction = TreeNodeSelectAction.Expand;

this
.TreeView1.Nodes.Add(node);

AddTreeNode(node, nodeInfo);
}
}


private

void
AddTreeNode(TreeNode parentNode, BusinessScopeNodeInfo nodeInfo)
{
TreeNode treeNode =

null
;

foreach
(BusinessScopeNodeInfo subNodeInfo
in
nodeInfo.Children)
{
treeNode =

new
TreeNode(subNodeInfo.Name);
treeNode.SelectAction = TreeNodeSelectAction.Expand;
parentNode.ChildNodes.Add(treeNode);

AddTreeNode(treeNode, subNodeInfo);
}
}


protected

void
btnSelect_Click(
object
sender, ImageClickEventArgs e)
{

string
result =
""
;

foreach
(TreeNode parent
in

this
.TreeView1.Nodes)
{

foreach
(TreeNode node
in
parent.ChildNodes)
{
StringBuilder sb =

new
StringBuilder();

foreach
(TreeNode subNode
in
node.ChildNodes)
{

if
(subNode.Checked)
{
sb.AppendFormat(

"{0},"
, subNode.Text);
}
}

if
(sb.Length >
0
)
{
sb.Insert(

0
,
string
.Format(
"{0}("
, node.Text));
sb.Append(

")"
);
result += sb.ToString().Replace(

",)"
,
")"
) +
";"
;
}

else

if
(node.Checked)
{
result += node.Text;
}
}
}
Helper.CloseWin(

this
, result.Trim(
';'
));
}




其中数的数据组装也是需要注意的一个地方,为了提高效率,避免频繁查找数据库,我们先把符合条件的数据放到
DataTable
,然后通过对象的
Select
在内存中查找,这样可以很好的提高递归函数的查找效率。






代码


///

<summary>


///

获取数据树


///

</summary>


///

<returns></returns>


public
ArrayList GetTree()
{
ArrayList arrReturn =

new
ArrayList();

string
sql =
string
.Format(
"Select * From {0} Order By PID, Seq "
, tableName);
Database db = DatabaseFactory.CreateDatabase();
DbCommand cmdWrapper = db.GetSqlStringCommand(sql);

DataSet ds = db.ExecuteDataSet(cmdWrapper);

if
(ds.Tables.Count >
0
)
{
DataTable dt = ds.Tables[

0
];
DataRow[] dataRows = dt.Select(

string
.Format(
" PID = {0}"
, -
1
));

for
(
int
i =
0
; i < dataRows.Length; i++)
{

int
id = Convert.ToInt32(dataRows[i][
"ID"
]);
BusinessScopeNodeInfo menuNodeInfo = GetNode(id, dt);
arrReturn.Add(menuNodeInfo);
}
}


return
arrReturn;
}



private
BusinessScopeNodeInfo GetNode(
int
id, DataTable dt)
{
BusinessScopeInfo menuInfo =

this
.FindByID(id);
BusinessScopeNodeInfo menuNodeInfo =

new
BusinessScopeNodeInfo(menuInfo);

DataRow[] dChildRows = dt.Select(

string
.Format(
" PID={0}"
, id));


for
(
int
i =
0
; i < dChildRows.Length; i++)
{

int
childId = Convert.ToInt32(dChildRows[i][
"ID"
]);
BusinessScopeNodeInfo childNodeInfo = GetNode(childId, dt);
menuNodeInfo.Children.Add(childNodeInfo);
}

return
menuNodeInfo;
}




其中所用到的数据实体如下面两个类所示,其中
BusinessScopeNodeInfo
是对象
BusinessScopeInfo
的进一步封装,方便提供树的基本信息,也就是
BusinessScopeNodeInfo
是一个包含了子类数据的对象,
BusinessScopeInfo
仅仅是数据库对象的映射实体。











代码


///

<summary>


///
BusinessScopeNodeInfo

的摘要说明。


///

</summary>


public

class
BusinessScopeNodeInfo : BusinessScopeInfo
{

private
ArrayList m_Children =
new
ArrayList();


///

<summary>


///

子菜单实体类对象集合


///

</summary>


public
ArrayList Children
{

get
{
return
m_Children; }

set
{ m_Children = value; }
}


public
BusinessScopeNodeInfo()
{

this
.m_Children =
new
ArrayList();
}


public
BusinessScopeNodeInfo(BusinessScopeInfo scopeInfo)
{

base
.Id = scopeInfo.Id;

base
.Name = scopeInfo.Name;

base
.Seq = scopeInfo.Seq;
}
}








代码

[Serializable]

public

class
BusinessScopeInfo : BaseEntity
{

#region
Field Members


private

decimal
m_Id =
0
;

private

decimal
m_Pid = -
1
;

private

string
m_Name =
""
;

private

string
m_Seq =
""
;


#endregion



#region
Property Members


public

virtual

decimal
Id
{

get

{

return

this
.m_Id;
}

set

{

this
.m_Id = value;
}
}


public

virtual

decimal
Pid
{

get

{

return

this
.m_Pid;
}

set

{

this
.m_Pid = value;
}
}


public

virtual

string
Name
{

get

{

return

this
.m_Name;
}

set

{

this
.m_Name = value;
}
}


public

virtual

string
Seq
{

get

{

return

this
.m_Seq;
}

set

{

this
.m_Seq = value;
}
}



#endregion


}



其中的数据格式大致如下
(
本文的例子是在
Oracle
环境中工作的),其实
SqlServer
或者其他数据库也是一样。



你是否碰到你的收藏夹凌乱不堪,或者很多收藏网页地址虽然分类,可是使用起来却感觉不太好,有没有想过把你的收藏夹内容自动生成一个网页的导航页面生成类似Hao123(
http://www.hao123.com/
),或者番茄花园导航(
http://www.tomatolei.com/
)一样的网页,用来迅速导航到自己的网址呢?

即使使用场景有争议,不过我们也来关注一下这种自动生成页面的技术吧,说不定可以使用在合适的环境下,如下面我测试了两个类别的收藏夹Favorite目录下,其中“搜索引擎”和“新闻网络”两个目录是我的测试收藏夹分类,Icon和Images是我构造生成主页所需要的一些内容目录。我们根据Index.htm来生成index.htm来作为导航的主页面。

由于我们不想改变收藏夹的IE快捷方式的内容,因此收藏夹里面的内容还是以IE快捷方式来保存,如下图所示。

其中IE快捷方式的内容如下所示,文件时采用INI格式进行保存的。

[DEFAULT]
BASEURL=http://g.cn
[InternetShortcut]
URL=http://g.cn

其中的Default中的BaseURL是域名地址,InternetShortcut中的URL是具体的网页地址,基本上这些内容就可以构造成一个IE快捷方式了。

整体运行的效果如下图所示。其中内容可以动态生成,由于采用了NVelocity引擎来生成内容,速度非常快,和你直接浏览一个html页面的速度相差无几。

下面我们来具体介绍下如何实现这个页面的自动生成吧。

单击Home主页的时候,我们就自动生成一个页面,生成代码如下所示。



代码


///

<summary>


///
导航到主页面

///

</summary>



public

void
GoHome()
{

string
tempFile
=

"
Favorite/index.txt
"
;

try

{
HelperClassAdapter helper

=

new
HelperClassAdapter(tempFile);
helper.Execute();

//
MessageUtil.ShowTips("生成成功");




bool
found
=

false
;

foreach
(IDockContent content
in

this
.dockPanel.Documents)
{
MainIE form

=
content
as
MainIE;

if
(form
!=

null

&&
form.extendedWebBrowser1.DocumentTitle
==

"
收藏夹导航
"
)
{
found

=

true
;
form.Activate();

break
;
}
}

if
(
!
found)
{

this
._currentUrl
=

"
my:home
"
;
AddMainIE();
}
}

catch
(Exception ex)
{
LogHelper.Error(ex);
MessageUtil.ShowTips(ex.Message);
}
}

这里其实主要的就是生成代码的类HelperClassAdapter,这个类通过获得模版文件,通过Nevelocity引擎把指定格式的内容附加到模版文件中,然后输出文件即可。



代码


public

class
HelperClassAdapter : AbstractAdapter
{

public
HelperClassAdapter(
string
templateFile)
:

base
(templateFile)
{
}


public

override

void
Execute()
{

string
startPath
=
Path.Combine(Application.StartupPath,
"
Favorite
"
);

if
(
!
Directory.Exists(startPath))
{
Directory.CreateDirectory(startPath);
}


//
遍历目录和文件,根据文件模版输出到文件中




#region
根据目录放置的字典

Dictionary

<
string
, List
<
FavoriteInfo
>>
fileDict
=

new
Dictionary
<
string
, List
<
FavoriteInfo
>>
();

string
[] dirArray
=
Directory.GetDirectories(startPath);

foreach
(
string
dir
in
dirArray)
{

string
dirName
=

new
DirectoryInfo(dir).Name;

if
(dirName.ToLower()
==

"
icon
"

||
dirName.ToLower()
==

"
images
"
)
//
系统图标跳过



continue
;


string
[] fileArray
=
Directory.GetFiles(dir);
List

<
FavoriteInfo
>
fileList
=

new
List
<
FavoriteInfo
>
();
FavoriteInfo info

=

null
;

foreach
(
string
file
in
fileArray)
{

if
(
!
string
.IsNullOrEmpty(file))
{
info

=

new
FavoriteInfo();
info.Category

=
dirName;
info.Title

=

new
FileInfo(file).Name.Replace(
"
.url
"
,
""
);

INIFileUtil iniUtil

=

new
INIFileUtil(file);
info.Url

=
iniUtil.IniReadValue(
"
InternetShortcut
"
,
"
URL
"
);

fileList.Add(info);
}

}

if
(
!
fileDict.ContainsKey(dir))
{
fileDict.Add(dir, fileList);
}
}

#endregion



//
设置输出文件名称和页面变量,然后输出文件



this
.FileNameOfOutput
=
Path.Combine(Application.StartupPath,
"
Favorite/index.htm
"
);
context.Put(

"
FileDict
"
, fileDict);
OutputFile();
}
}

其中模版文件index.txt文件很简单,部分关键代码如下所示,其中主要关注有$开始的运算符和变量,这些代码是实现对象到页面内容的转换的。其中#foreach($FavoriteList in ${FileDict.Values})是遍历里面的对象,进行循环处理的,语法也很简单。



代码

<
DIV
id
=content
>


<!--
/ sidebar
--><!--
/ board
-->


<
div
class
=box
id
=coolsites
>


<
h2
align
="center"
class
="style2"
>
收藏夹导航
<
a
class
=""
id
=cslayout
title
=居中对齐/左对齐
>
居中对齐/左对齐
</
a
></
h2
>


<
div
id
=list
>

#foreach($FavoriteList in ${FileDict.Values})

<
dl
>

#set($count = 0)
#foreach($FavoriteInfo in $FavoriteList)
#if($count==0)

<
dt
><
a
href
="#"
>
[ ${FavoriteInfo.Category.ToString()} ]
</
a
>

#end
#set($count = $count + 1)

<
dd
><
a
href
="${FavoriteInfo.Url.ToString()}"
>
${FavoriteInfo.Title.ToString()}
</
a
>

#end

</
dd
></
dl
>

#end

</
div
>


</
div
>


</
div
>

另外FavoriteInfo是一些基本属性的实体类,代码很简单:



代码


public

class
FavoriteInfo
{

private

string
m_category;
//
目录分类名称



private

string
m_title;
//
网页标题



private

string
m_url;
//
网页链接




///

<summary>


///
目录分类名称

///

</summary>



public

string
Category
{

get
{
return
m_category; }

set
{ m_category
=
value; }
}


///

<summary>


///
网页标题

///

</summary>



public

string
Title
{

get
{
return
m_title; }

set
{ m_title
=
value; }
}


///

<summary>


///
网页链接

///

</summary>



public

string
Url
{

get
{
return
m_url; }

set
{ m_url
=
value; }
}
}

HelperClassAdapter的基类如下所示,主要是定义一些公共的属性变量及操作,方便被继承成各种类型的子类。



代码


public

abstract

class
AbstractAdapter : IAdapter
{

#region
保护成员变量



protected
VelocityContext context;

protected
Template template;


protected

string
templateFile;


protected

const

string
NVELOCITY_PROPERTY
=

"
nvelocity.properties
"
;


#endregion



#region
成员变量及公共属性



private

string
fileExtension
=

"
.htm
"
;
//
输出的文件后缀名,如".cs"。



private

string
directoryOfOutput
=

"
Articles
"
;
//
输出文件的文件夹名称



private

string
fileNameOfOutput;
//
输出的文件名称




///

<summary>


///
输出文件的文件夹名称, 如"Entity","Common"等

///

</summary>



public

string
DirectoryOfOutput
{

set
{ directoryOfOutput
=
value; }

get
{
return
directoryOfOutput; }
}


///

<summary>


///
输出的文件名称. 如果想输出的文件为 "A.cs", 那么文件名为"A".

///
默认的文件名称为模板文件的名称,没有后缀名.

///

</summary>



public

string
FileNameOfOutput
{

set
{ fileNameOfOutput
=
value; }

get
{
return
fileNameOfOutput; }
}


///

<summary>


///
输出的文件后缀名,如".cs"。

///

</summary>



public

string
FileExtension
{

get
{
return
fileExtension; }

set
{ fileExtension
=
value; }
}


#endregion



///

<summary>


///
根据参数初始化基类的Adapter

///

</summary>


///

<param name="templateFile">
模板文件名称
</param>



public
AbstractAdapter(
string
templateFile)
{

#region
判断输入的参数是否有效



if
(templateFile
==

null
)
{

const

string
error
=

"
Invalid parameter of templateFile.
"
;

LogHelper.Error(error);

throw

new
ArgumentException(error,
"
templateFile
"
);
}


#endregion



this
.templateFile
=
templateFile;

InitTemplateEngine();

//
初始化模板引擎



fileNameOfOutput

=
GetFileNameFromTemplate(templateFile);
//
默认情况下, 输出的文件名称为模板名称


directoryOfOutput
=

"
Articles
"
;
//
默认情况下,放到Common目录下面


}


///

<summary>


///
创建输出到文件中的方法

///

</summary>



public

virtual

void
Execute()
{
}



///

<summary>


///
根据模板创建输出的文件

///

</summary>



public

virtual

void
OutputFile()
{

if
(template
!=

null
)
{

string
filePath
=
Path.GetDirectoryName(
this
.fileNameOfOutput);
DirectoryInfo dir

=

new
DirectoryInfo(filePath);

if
(
!
dir.Exists)
{
dir.Create();
}

LogHelper.Debug(

string
.Format(
"
Class file output path:{0}
"
,
this
.fileNameOfOutput));

using
(StreamWriter writer
=

new
StreamWriter(
this
.fileNameOfOutput,
false
))
{
template.Merge(context, writer);
}
}
}


///

<summary>


///
根据模板创建代码输出到字符串流中

///

</summary>


///

<returns>
根据模板生成的代码
</returns>



public

virtual

string
OutputString()
{

string
code
=

string
.Empty;


if
(template
!=

null
)
{

using
(StringWriter writer
=

new
StringWriter())
{
template.Merge(context, writer);
code

=
writer.GetStringBuilder().ToString();
}
}

return
code;
}


///

<summary>


///
初始化NVelocity模板引擎并加载程序的配置信息e

///

</summary>



protected

void
InitTemplateEngine()
{
Velocity.Init(NVELOCITY_PROPERTY);

//
Velocity.Init();


//
Make a context object and populate with the data.  This is where the Velocity engine

//
gets the data to resolve the references (ex. $foreignList) in the template


context
=

new
VelocityContext();


try

{
template

=
Velocity.GetTemplate(templateFile);
}

catch
(ResourceNotFoundException re)
{

string
error
=

string
.Format(
"
Cannot find template
"

+
templateFile);

LogHelper.Error(error);

throw

new
Exception(error, re);
}

catch
(ParseErrorException pee)
{

string
error
=

string
.Format(
"
Syntax error in template
"

+
templateFile
+

"
:
"

+
pee.StackTrace);

LogHelper.Error(error);

throw

new
Exception(error, pee);
}

}


#region
辅助方法



///

<summary>


///
从模板文件名称获取输出文件名的方法

///

</summary>


///

<param name="templateFileName">
带后缀名的模板文件名
</param>


///

<returns>
输出的文件名(无后缀名)
</returns>



private

string
GetFileNameFromTemplate(
string
templateFileName)
{

int
sindex1
=
templateFileName.LastIndexOf(
'
/
'
);

int
sindex2
=
templateFileName.LastIndexOf(
'
\\
'
);


int
sindex
=
(sindex1
>
sindex2)
?
sindex1 : sindex2;

int
eindex
=
templateFileName.IndexOf(
'
.
'
);


if
(sindex
<
eindex)
{

return
templateFileName.Substring(sindex
+

1
, eindex
-
sindex
-

1
);
}

else

{

return
templateFileName.Substring(sindex);
}
}


#endregion

}


public

interface
IAdapter
{

///

<summary>


///
创建输出到文件中的方法

///

</summary>



void
Execute();
}

另外,读取INI文件,需要的辅助类代码INIFileUtil如下所示。



代码


public

class
INIFileUtil
{

public

string
path;


public
INIFileUtil(
string
INIPath)
{
path

=
INIPath;
}

[DllImport(

"
kernel32
"
)]

private

static

extern

long
WritePrivateProfileString(
string
section,
string
key,
string
val,
string
filePath);

[DllImport(

"
kernel32
"
)]

private

static

extern

int
GetPrivateProfileString(
string
section,
string
key,
string
def, StringBuilder retVal,
int
size,
string
filePath);


[DllImport(

"
kernel32
"
)]

private

static

extern

int
GetPrivateProfileString(
string
section,
string
key,
string
defVal, Byte[] retVal,
int
size,
string
filePath);



///

<summary>


///
写INI文件

///

</summary>


///

<param name="Section"></param>


///

<param name="Key"></param>


///

<param name="Value"></param>



public

void
IniWriteValue(
string
Section,
string
Key,
string
Value)
{
WritePrivateProfileString(Section,Key,Value,

this
.path);
}


///

<summary>


///
读取INI文件

///

</summary>


///

<param name="Section"></param>


///

<param name="Key"></param>


///

<returns></returns>



public

string
IniReadValue(
string
Section,
string
Key)
{
StringBuilder temp

=

new
StringBuilder(
255
);

int
i
=
GetPrivateProfileString(Section,Key,
""
,temp,
255
,
this
.path);

return
temp.ToString();
}


public

byte
[] IniReadValues(
string
section,
string
key)
{

byte
[] temp
=

new

byte
[
255
];

int
i
=
GetPrivateProfileString(section, key,
""
, temp,
255
,
this
.path);

return
temp;

}


///

<summary>


///
删除ini文件下所有段落

///

</summary>



public

void
ClearAllSection()
{
IniWriteValue(

null
,
null
,
null
);
}

///

<summary>


///
删除ini文件下personal段落下的所有键

///

</summary>


///

<param name="Section"></param>



public

void
ClearSection(
string
Section)
{
IniWriteValue(Section,

null
,
null
);
}
}

在.net里面,有一个WebBrowser控件,这个控件可以用于很多用途,很多人用来定做自己的浏览器,本文谈谈如何获取默认浏览器的设置,并介绍如何设置自己的浏览器为默认浏览器的小技巧。

先看一个小的界面图形,用来更好理解这个功能的用途,如下图我们可以看到需要获取到系统的默认浏览器名称,并提供设置默认浏览器功能。

其实,这些操作都是和注册表相关的内容,这些内容保存在键HKEY_CLASSES_ROOT\http\shell\open\command中的默认值里面,

而默认浏览器的名称保存在HKEY_CLASSES_ROOT\http\shell\open\ddeexec\Application的默认值里面。

基本上我们修改这两个键值就可以实现默认浏览器的设置了。

在上面的默认浏览器界面中,我封装了一些方法,现发布出来,希望对大家有用处。



代码


private

void
FrmOptions_Load(
object
sender, EventArgs e)
{
InitDefaultBrowser();
}


private

void
InitDefaultBrowser()
{

string
defaultName
=
RegistryHelper.GetDefaultBrowerName();

try

{

string
browserPath
=
CRegex.GetText(defaultName,
"
\
"
(
?<
key
>
.
*?
)\
""
,
1
);
FileInfo fileInfo

=

new
FileInfo(browserPath);

string
fileName
=
fileInfo.Name.Replace(fileInfo.Extension,
""
);

this
.lblDefaultName.Text
=
fileName;
}

catch
(Exception ex)
{
LogHelper.Error(ex);
}
}


private

void
btnSetDefault_Click(
object
sender, EventArgs e)
{
RegistryHelper.SetDefaultBrowser(Application.ExecutablePath);
MessageUtil.ShowTips(

"
设置默认浏览器成功
"
);
InitDefaultBrowser();
}


private

void
btnReset_Click(
object
sender, EventArgs e)
{
RegistryHelper.ResetIEDefaultBrowser();
MessageUtil.ShowTips(

"
恢复IE为默认浏览器成功
"
);
InitDefaultBrowser();
}

其中我把注册表的访问,放到了一个独立的注册版访问类中进行处理了,其中包含了3个部分,获取默认浏览器名称,设置自己的浏览器为默认浏览器,恢复IE为默认浏览器功能。下面是该类的代码。



代码


///

<summary>


///
获取默认浏览器的名称

///

</summary>


///

<returns></returns>



public

static

string
GetDefaultBrowerName()
{

string
mainKey
=

@"
http\shell\open\command
"
;

string
nameKey
=

@"
http\shell\open\ddeexec\Application
"
;


string
strRet
=

string
.Empty;

try

{
RegistryKey regKey

=
Registry.ClassesRoot.OpenSubKey(mainKey);
strRet

=
regKey.GetValue(
""
).ToString();
}

catch

{
strRet

=

""
;
}

return
strRet;
}


///

<summary>


///
设置自定义浏览器为默认浏览器

///

</summary>


///

<param name="browserExePath"></param>


///

<returns></returns>



public

static

bool
SetDefaultBrowser(
string
browserExePath)
{

string
mainKey
=

@"
http\shell\open\command
"
;

string
nameKey
=

@"
http\shell\open\ddeexec\Application
"
;

bool
result
=

false
;


try

{

string
value
=

string
.Format(
"
\
"
{
0
}\
"
\
"
%
1
\
""
, browserExePath);
RegistryKey regKey

=
Registry.ClassesRoot.OpenSubKey(mainKey,
true
);
regKey.SetValue(

""
, value);
regKey.Close();

FileInfo fileInfo

=

new
FileInfo(browserExePath);

string
fileName
=
fileInfo.Name.Replace(fileInfo.Extension,
""
);
regKey

=
Registry.ClassesRoot.OpenSubKey(nameKey,
true
);
regKey.SetValue(

""
, fileName);
regKey.Close();

result

=

true
;
}

catch
(Exception ex)
{
LogHelper.Error(ex);
}


return
result;
}


///

<summary>


///
恢复IE为默认浏览器

///

</summary>


///

<returns></returns>



public

static

bool
ResetIEDefaultBrowser()
{

string
mainKey
=

@"
http\shell\open\command
"
;

string
nameKey
=

@"
http\shell\open\ddeexec\Application
"
;

string
IEPath
=

@"
C:\Program Files\Internet Explorer\iexplore.exe
"
;

bool
result
=

false
;


try

{

string
value
=

string
.Format(
"
\
"
{
0
}\
"
-- \
"
%
1
\
""
, IEPath);
RegistryKey regKey

=
Registry.ClassesRoot.OpenSubKey(mainKey,
true
);
regKey.SetValue(

""
, value);
regKey.Close();

regKey

=
Registry.ClassesRoot.OpenSubKey(nameKey,
true
);
regKey.SetValue(

""
,
"
IExplore
"
);
regKey.Close();

result

=

true
;
}

catch

{
}


return
result;
}

另外代码中,为了获取到默认的浏览器文件名称,我通过正则表达式来获取内容,

我们看到,那个默认值是:"E:\Program Files\Maxthon2\Maxthon.exe" "%1"

为了获得第一部分的内容,我通过正则表达式匹配方式来获取,如代码

string
browserPath
=
CRegex.GetText(defaultName,
"
\
"
(
?<
key
>
.
*?
)\
""
,
1
);

该方法的详细内容如下:



代码


///

<summary>


///
单个匹配内容

///

</summary>


///

<param name="sInput">
输入内容
</param>


///

<param name="sRegex">
表达式字符串
</param>


///

<param name="iGroupIndex">
分组序号, 从1开始, 0不分组
</param>



public

static

string
GetText(
string
sInput,
string
sRegex,
int
iGroupIndex)
{
Regex re

=

new
Regex(sRegex, RegexOptions.IgnoreCase
|
RegexOptions.IgnorePatternWhitespace
|
RegexOptions.Multiline);
Match mc

=
re.Match(sInput);

if
(mc.Success)
{

if
(iGroupIndex
>

0
)

return
mc.Groups[iGroupIndex].Value;

else


return
mc.Value;
}

else


return

""
;
}

以上是一点关于设置默认浏览器的小技巧,留底,供娱乐,搏一声好,足矣!