2023年2月

在数据仓库中,无一例外地需要和时间维度打交道,因此设计合理的时间维度,也是一个数据仓库项目开始必备的资源储备,如果有这方面的积累,就不用到处寻找合适的设计模型以及存储过程的代码了,否则可能需要花费一定的时间去寻找符合该项目合适的存储过程,或者自己动手编写。

一般来说,时间维度的创建要根据实际的数据仓库应用来,基本上可以分为天、月的时间维度表,更细的可以分为半小时时间段,小时时间段等等,一般数据量大的时间维度都是利用存储过程来生成的。

下面介绍一些时间维度表的设计结构。

<1> 时间维度表1(两列都是字段)
维度属性
维度属性
日期关键字
日历年月
日期完全描述
日历季度
星期
日历半年度
纪元日编号
日历年
纪元周编号
财政周
纪元月编号
年度财政周数
日历日期编号
财政月
日历周编号
年度财政月数
日历月编号
财政年月
财政月日编号
财政季度
财政周编号
财政季年度
财政月编号
财政半年度
周末指示符
财政年
月末指示符
节假日指示符
日历周结束日期
星期指示符
年度日历周数
销售时令
日历月名
重大事件
年度日历月数
其它

<1> 时间维度表2
a.日期维度

名称

代码

数据类型

主外键

序列号

C_ID

int

P

日期

C_DATE

datetime

 

星期

C_WEEK

varchar(12)

 

月份

C_MONTH

varchar(2)

 

C_YEAR

varchar(4)

 

季度

C_QUARTER

varchar(4)

 

b.半小时时间维度

名称

代码

数据类型

主外键

序列号

C_ID

Varchar(50)


时段

C_SHIDUAN

Varchar(4000)


c.时间段维度

名称

代码

数据类型

主外键

序列号

C_ID

int

P

时间段

C_Section

varchar(50)

 

开始时间

C_Begin

varchar(50)

 

结束时间

C_End

varchar(50)

 

3、比较完整的时间维度设计

Name

Type

Nullable

Comments

DAY_CODE

VARCHAR2(8)

N

日代码

DAY_LONG_DESC

VARCHAR2(30)

N

日完整名称

DAY_MEDIUM_DESC

VARCHAR2(30)

N

日中等长度名

DAY_SHORT_DESC

VARCHAR2(30)

N

日短名

WEEK_CODE

VARCHAR2(8)

N

周代码

WEEK_LONG_DESC

VARCHAR2(30)

N

周完整名称

WEEK_MEDIUM_DESC

VARCHAR2(30)

N

周中等长度名

WEEK_SHORT_DESC

VARCHAR2(30)

N

周短名

TEN_DAY_CODE

VARCHAR2(8)

N

旬代码

TEN_DAY_LONG_DESC

VARCHAR2(30)

N

旬完整名称

TEN_DAY_MEDIUM_DESC

VARCHAR2(30)

N

旬中等长度名

TEN_DAY_SHORT_DESC

VARCHAR2(30)

N

旬短名

MONTH_CODE

VARCHAR2(8)

N

月代码

MONTH_LONG_DESC

VARCHAR2(30)

N

月完整名称

MONTH_MEDIUM_DESC

VARCHAR2(30)

N

月中等长度名

MONTH_SHORT_DESC

VARCHAR2(30)

N

月短名

QUARTER_CODE

VARCHAR2(8)

N

季代码

QUARTER_LONG_DESC

VARCHAR2(30)

N

季完整名称

QUARTER_MEDIUM_DESC

VARCHAR2(30)

N

季中等长度名

QUARTER_SHORT_DESC

VARCHAR2(30)

N

季短名

HALF_YEAR_CODE

VARCHAR2(8)

N

半年代码

HALF_LONG_DESC

VARCHAR2(30)

N

半年完整名称

HALF_MEDIUM_DESC

VARCHAR2(30)

N

半年中等长度名

HALF_SHORT_DESC

VARCHAR2(30)

N

半年短名

YEAR_CODE

VARCHAR2(8)

N

年代码

YEAR_LONG_DESC

VARCHAR2(30)

N

年完整名称

YEAR_MEDIUM_DESC

VARCHAR2(30)

N

年中等长度名

YEAR_SHORT_DESC

VARCHAR2(30)

N

年短名

ALL_TIME_CODE

VARCHAR2(8)

N

全部时间代码

ALL_TIME_DESC

VARCHAR2(30)

N

全部时间名称

DAY_TIMESPAN

NUMBER(3)

N

日时间跨天

DAY_END_DATE

VARCHAR2(8)

N

结束日期

WEEK_TIMESPAN

NUMBER(3)

N

周跨天数

WEEK_END_DATE

VARCHAR2(8)

N

周结束日期

TEN_DAY_TIMESPAN

NUMBER(3)

N

旬跨天数

TEN_DAY_END_DATE

VARCHAR2(8)

N

旬结束日期

MONTH_TIMESPAN

NUMBER(3)

N

月跨天数

MONTH_END_DATE

VARCHAR2(8)

N

月结束日期

QUARTER_TIMESPAN

NUMBER(3)

N

季跨天数

QUARTER_END_DATE

VARCHAR2(8)

N

季结束日期

HALF_YEAR_TIMESPAN

NUMBER(3)

N

半年跨天数

HALF_YEAR_END_DATE

VARCHAR2(8)

N

半年结束日期

YEAR_TIMESPAN

NUMBER(3)

N

年跨天数

YEAR_END_DATE

VARCHAR2(8)

N

年结束日期

BDISABLED

CHAR(1)

N

 



构建时间维度的存储过程包体如下所示
代码


create or replace package body PKG_LOAD_DIM_TIME
as
  
/************************************
  ** 是否合理自然日
  ** 如果是合理自然日则返回1,否则返回0
  ***********************************
*/
  
function F_Is_Day_ID
  (
    p_TIME_ID DIW.DW_DIM_TIME.DAY_CODE
%type
  ) 
return number
  
is
    v_Date date;
  
begin
    v_Date :
= to_date(p_TIME_ID, 'YYYYMMDD');
    
return 1;
  exception
    
when others then
      
return 0;
  
end F_Is_Day_ID;

  
/************************************
  ** 加载时间维度数据
  ***********************************
*/
  
procedure P_BUILD_DIM_TIME
  (
    p_START_DATE DIW.DW_DIM_TIME.DAY_CODE
%type,  --开始日期
    p_END_DATE DIW.DW_DIM_TIME.DAY_CODE%type     --结束日期
  )
  
is
    v_START_DATE date;  
--开始日期
    v_END_DATE date;    --结束日期
    v_DATE date;

    v_YEAR 
varchar2(4);
    v_QUARTER 
varchar2(1);
    v_MONTH 
varchar2(2);
    v_TEN_DAY 
varchar2(1);
    v_WEEK 
varchar2(2);
    v_WEEK_YEAR 
varchar2(4);
    v_DAY 
varchar2(2);

  
begin
    
--BICODE.PKG_ETL_CONTROLLER.P_ETL_WRITE_LOG('一般','BICODE.PKG_LOAD_DIM_TIME.P_BUILD_DIM_TIME执行开始...');
    execute immediate 'TRUNCATE TABLE DIW.ODS_DIM_TIME';

    
if F_Is_Day_ID(p_START_DATE)=1 and F_Is_Day_ID(p_END_DATE)=1 then

        v_START_DATE :
= TO_DATE(p_START_DATE,'YYYYMMDD');
        v_END_DATE :
= TO_DATE(p_END_DATE,'YYYYMMDD');

        
if v_END_DATE - v_START_DATE >= 0 then
          
--开始生成
          for i in 0..v_END_DATE - v_START_DATE loop

            v_DATE :
= v_START_DATE+i;
            v_YEAR :
= TO_CHAR(v_DATE,'YYYY');
            v_QUARTER :
= TO_CHAR(v_DATE,'Q');
            v_MONTH :
= TO_CHAR(v_DATE,'MM');
            v_TEN_DAY :
= case
                           
when TO_CHAR(v_DATE,'DD')<'11' then '1'
                           
when TO_CHAR(v_DATE,'DD')<'21' then '2'
                           
when TO_CHAR(v_DATE,'DD')<'32' then '3'
                         
end;
            
--周方案一:星期被年分开,实际是7天分段不是自然周
            --v_WEEK := TO_CHAR(v_DATE,'WW');
            --v_WEEK_YEAR := v_YEAR;

            
--周方案二:本星期星期四所在年
            --v_WEEK := TO_CHAR(v_DATE,'IW');
            --select TO_CHAR(NEXT_DAY(v_DATE-7,2)+3,'YYYY') into v_WEEK_YEAR from dual;
            ----v_WEEK_YEAR := TO_CHAR(NEXT_DAY(v_DATE-7,2)+3,'YYYY');--奇怪的错误??

            
--周方案三:自然周,周被年分,没找到函数支持。
            select
              LPAD(TO_CHAR(TRUNC((v_DATE
              
-to_date(v_YEAR||'0101','yyyymmdd')
              
+DECODE(TO_NUMBER(to_CHAR(to_date(v_YEAR||'0101','yyyymmdd'),'D')),1,7,
              TO_NUMBER(to_CHAR(to_date(v_YEAR
||'0101','yyyymmdd'),'D'))-1)-1)/7+1)),2,'0')
              
INTO v_WEEK
            
from dual;
            v_WEEK_YEAR :
= v_YEAR;

            v_DAY :
= TO_CHAR(v_DATE,'DD');

            
insert into DIW.DW_DIM_TIME
            (
              DAY_CODE,
              DAY_LONG_DESC,
              DAY_MEDIUM_DESC,
              DAY_SHORT_DESC,
              WEEK_CODE,
              WEEK_LONG_DESC,
              WEEK_MEDIUM_DESC,
              WEEK_SHORT_DESC,
              TEN_DAY_CODE,
              TEN_DAY_LONG_DESC,
              TEN_DAY_MEDIUM_DESC,
              TEN_DAY_SHORT_DESC,
              MONTH_CODE,
              MONTH_LONG_DESC,
              MONTH_MEDIUM_DESC,
              MONTH_SHORT_DESC,
              QUARTER_CODE,
              QUARTER_LONG_DESC,
              QUARTER_MEDIUM_DESC,
              QUARTER_SHORT_DESC,
              HALF_YEAR_CODE,
              HALF_LONG_DESC,
              HALF_MEDIUM_DESC,
              HALF_SHORT_DESC,
              YEAR_CODE,
              YEAR_LONG_DESC,
              YEAR_MEDIUM_DESC,
              YEAR_SHORT_DESC,
              ALL_TIME_CODE,
              ALL_TIME_DESC,
              DAY_TIMESPAN,
              DAY_END_DATE,
              WEEK_TIMESPAN,
              WEEK_END_DATE,
              TEN_DAY_TIMESPAN,
              TEN_DAY_END_DATE,
              MONTH_TIMESPAN,
              MONTH_END_DATE,
              QUARTER_TIMESPAN,
              QUARTER_END_DATE,
              HALF_YEAR_TIMESPAN,
              HALF_YEAR_END_DATE,
              YEAR_TIMESPAN,
              YEAR_END_DATE
            )
            
values
            (
              v_YEAR
||v_MONTH||v_DAY,
              v_YEAR
||''||v_MONTH||''||v_DAY||'',
              
/*v_MONTH||'月'||*/v_DAY||'',
              v_YEAR
||'-'||v_MONTH||'-'||v_DAY,
              v_WEEK_YEAR
||'W'||v_WEEK,
              v_WEEK_YEAR
||'年第'||v_WEEK||'',
              
''||v_WEEK||'',
              v_WEEK_YEAR
||'-W'||v_WEEK,
              v_YEAR
||v_MONTH||'X'||v_TEN_DAY,
              v_YEAR
||''||v_MONTH||''||decode(v_TEN_DAY,'1','','2','','')||'',
              decode(v_TEN_DAY,
'1','','2','','')||'',
              v_YEAR
||'-'||v_MONTH||'-X'||v_TEN_DAY,
              v_YEAR
||v_MONTH,
              v_YEAR
||''||v_MONTH||'',
              v_MONTH
||'',
              v_YEAR
||'-'||v_MONTH,
              v_YEAR
||'Q'||v_QUARTER,
              v_YEAR
||'年第'||v_QUARTER||'季度',
              
''||v_QUARTER||'季度',
              v_YEAR
||'-'||'Q'||v_QUARTER,
              v_YEAR
||'H'||decode(v_QUARTER,'1','1','2','1','2'),
              v_YEAR
||''||decode(v_QUARTER,'1','','2','','')||'半年',
              decode(v_QUARTER,
'1','','2','','')||'半年',
              v_YEAR
||'-'||'H'||decode(v_QUARTER,'1','1','2','1','2'),
              v_YEAR,
              v_YEAR
||'',
              v_YEAR
||'',
              v_YEAR,
              
'ALL',
              
'ALL_TIME',
              
1,
              v_YEAR
||v_MONTH||v_DAY,
              
case
                
when
                  TO_CHAR(NEXT_DAY(v_DATE
-7,2),'YYYY')<v_WEEK_YEAR
                
then
                  NEXT_DAY(v_DATE,
2)-TO_DATE(v_WEEK_YEAR||'0101','YYYYMMDD')
                
when
                  TO_CHAR(NEXT_DAY(v_DATE,
2),'YYYY')>v_WEEK_YEAR
                
then
                  TO_DATE(v_WEEK_YEAR
||'1231','YYYYMMDD')-NEXT_DAY(v_DATE-7,2)+1
                
else 7
              
end,
              
case
                
when
                  TO_CHAR(NEXT_DAY(v_DATE,
2),'YYYY')>v_WEEK_YEAR
                
then v_WEEK_YEAR||'1231'
                
else TO_CHAR(NEXT_DAY(v_DATE,2)-1,'YYYYMMDD')
              
end,
              decode(v_TEN_DAY,
'3',LAST_DAY(v_DATE)-TO_DATE(v_YEAR||v_MONTH||'21','YYYYMMDD')+1,10),
              decode(v_TEN_DAY,
'3',TO_CHAR(LAST_DAY(v_DATE),'YYYYMMDD'),'2',v_YEAR||v_MONTH||'20',v_YEAR||v_MONTH||'10'),
              TO_NUMBER(TO_CHAR(LAST_DAY(v_DATE),
'DD')),
              TO_CHAR(LAST_DAY(v_DATE),
'YYYYMMDD'),
              decode(v_QUARTER,
'1',TO_DATE(v_YEAR||'0331','YYYYMMDD')-TO_DATE(v_YEAR||'0101','YYYYMMDD')+1,'2',91,92),
              decode(v_QUARTER,
'1',v_YEAR||'0331','2',v_YEAR||'0630','3',v_YEAR||'0930',v_YEAR||'1231'),
              decode(v_QUARTER,
'3',184,'4',184,TO_DATE(v_YEAR||'0630','YYYYMMDD')-TO_DATE(v_YEAR||'0101','YYYYMMDD')+1),
              decode(v_QUARTER,
'1',v_YEAR||'0630','2',v_YEAR||'0630',v_YEAR||'1231'),
              TO_DATE(v_YEAR
||'1231','YYYYMMDD')-TO_DATE(v_YEAR||'0101','YYYYMMDD')+1,
              v_YEAR
||'1231'
            )
            ;
          
end loop;
          
commit;
        
end if;
    
end if;
    
    
--BICODE.PKG_ETL_CONTROLLER.P_UPDATE_ETL_TIME_INFO('DW','DIM_TIME');
    --BICODE.PKG_ETL_CONTROLLER.P_ETL_WRITE_LOG('一般','BICODE.PKG_LOAD_DIM_TIME.P_BUILD_DIM_TIME执行结束...');

    exception
      
when others then
        
rollback;
        
--BICODE.PKG_ETL_CONTROLLER.P_ETL_WRITE_LOG('异常','BICODE.PKG_LOAD_DIM_TIME.P_BUILD_DIM_TIME执行失败...');    
    
  
end P_BUILD_DIM_TIME;
end PKG_LOAD_DIM_TIME;


有时候需要精确度到月份的时间维度表,由于上面的日期时间维度表相对信息比较丰富,可以建立一个视图来影射一个月度时间维度表,如下图所示:


具体的视图Sql如下所示:

 

代码

CREATE OR REPLACE VIEW DW_DIM_TIME_MONTH_V AS
SELECT DISTINCT
  T.MONTH_CODE,
  T.MONTH_LONG_DESC,
  T.MONTH_MEDIUM_DESC,
  T.MONTH_SHORT_DESC,
  T.QUARTER_CODE,
  T.QUARTER_LONG_DESC,
  T.QUARTER_MEDIUM_DESC,
  T.QUARTER_SHORT_DESC,
  T.HALF_YEAR_CODE,
  T.HALF_LONG_DESC,
  T.HALF_MEDIUM_DESC,
  T.HALF_SHORT_DESC,
  T.YEAR_CODE,
  T.YEAR_LONG_DESC,
  T.YEAR_MEDIUM_DESC,
  T.YEAR_SHORT_DESC
FROM DIW.DW_DIM_TIME T;

 

 

 

吉日嘎拉,一个专注于通用权限管理系统的开发狂热者,在博客园是一个有争议的人物,不过从其文章数量及内容介绍,专注肯定不是浪得虚名,一个人把东西做的专注,也就意味着更多的投入及考虑,可以作为后来者更多的借鉴。

本人获其友情赠送一套源码,用作深入分析及提炼使用,初一接触,整个体系架构还是比较庞大,如下所示。

本文思路是源自对Winform窗体的分析研究,发现其中不少优点,很多闪亮的地方,结合本人在Winform方面的积累及研究,对即日嘎拉的代码进行进一步的提炼,于是撰写了本文,本文主要介绍继承窗体的封装使用。

我们先来分析下吉日嘎拉其对窗体的设计代码,一步步进行深入的研究。

首先其也是设计一个窗体基类BaseForm, 其他窗体继承自BaseForm基类,这也是一种常用的设计。


代码


///

<summary>


///
加载窗体

///

</summary>



private

void
FormOnLoad()
{

//
多语言国际化加载



this
.Localization(
this
);

//
获得权限



this
.GetPermission();

//
设置焦点



this
.ActiveControl
=

this
.txtCode;

this
.txtCode.Focus();
}

#endregion



private

void
FrmItemDetailsAdd_Load(
object
sender, EventArgs e)
{

if
(
!
this
.DesignMode)
{

//
设置鼠标繁忙状态



this
.Cursor
=
Cursors.WaitCursor;

try

{

this
.FormOnLoad();
}

catch
(Exception ex)
{

this
.ProcessException(ex);
}

finally

{

//
设置鼠标默认状态



this
.Cursor
=
Cursors.Default;
}
}
}


#region
private void SaveEntity(bool close)


///

<summary>


///
保存

///

</summary>


///

<param name="close">
关闭窗体
</param>



private

void
SaveEntity(
bool
close)
{

//
检查输入的有效性



if
(
this
.CheckInput())
{

//
设置鼠标繁忙状态



this
.Cursor
=
Cursors.WaitCursor;

try

{

if
(
this
.SaveEntity())
{

if
(close)
{

//
关闭窗口



this
.Close();
}

else

{

this
.ClearScreen();
}
}
}

catch
(Exception ex)
{

this
.ProcessException(ex);
}

finally

{

//
设置鼠标默认状态



this
.Cursor
=
Cursors.Default;
}
}
}

#endregion



private

void
btnAdd_Click(
object
sender, EventArgs e)
{

this
.SaveEntity(
false
);
}


private

void
btnSave_Click(
object
sender, EventArgs e)
{

this
.SaveEntity(
true
);
}


private

void
btnCancel_Click(
object
sender, EventArgs e)
{

this
.Close();
}

从其中的封装调用我们看到,吉日嘎拉已经把很多变化的代码抽象出来,也不失为一个较好的设计,另外对窗体的输入回车转Tab也做了一个通用的转换,如下所示:


代码


private

void
FrmItemDetailsAdd_KeyDown(
object
sender, KeyEventArgs e)
{

string
keyCode
=
e.KeyCode.ToString();

switch
(keyCode)
{

case

"
Enter
"
:

case

"
Return
"
:

if
(
this
.ActiveControl
is
TextBox)
{

if
(
!
((TextBox)
this
.ActiveControl).Multiline)
{
SendKeys.Send(

"
{TAB}
"
);
}
}

break
;
}

switch
(e.KeyValue)
{

case

116
:

//
点击了F5按钮



this
.FormOnLoad();

break
;
}
}

闪亮之处还有很多,本文暂时在此打住,介绍一下其中可以重构更好的地方。

由于吉日嘎拉的窗体设计,对于一般常用到的编辑数据、新增数据窗体,分开了两个不同的窗体进行处理,而且由于BaseForm窗体没有对通用的函数进行进一步的抽象,因此,编辑及新增窗体多了很多重复累赘的代码,其实可以把新增、编辑合并一个窗体,然后根据新增、编辑两种不同的条件进行处理即可。

由于BaseForm一般需要在大多数的窗体中,而新增编辑数据窗体一般较为特殊一点,可以再增加一个基类BaseEditForm,用来做新增编辑窗体的基类,该基类继承自BaseForm类,工程代码如下所示。

其中运行例子的效果如下所示:

编辑以及新增我们整合在一个窗体中,先看看该基类的设计视图,我们在其中添加了3个按钮(常用的添加、保存、关闭按钮)。

窗体的代码我大类采用了可重载的虚函数,留给子类窗体进行实现不同的处理操作,如窗体加载显示操作、显示数据到控件的操作、保存、新增等函数都是需要实现的,而调用逻辑以及一些通用的处理,则在基类BaseEditForm中实现,子类不用重复这些代码,按钮控件(添加、保存、关闭)的事件处理也已经进行了逻辑封装,如下所示。


代码


public

override

void
FormOnLoad()
{

base
.FormOnLoad();

if
(
!
this
.DesignMode)
{

if
(
!
string
.IsNullOrEmpty(ID))
{

this
.Text
=

"
编辑
"

+

this
.Text;

this
.btnAdd.Visible
=

false
;
//
如果是编辑,则屏蔽添加按钮


}

else

{

this
.Text
=

"
新建
"

+

this
.Text;
}

DisplayData();
}
}


///

<summary>


///
显示数据到控件上

///

</summary>



public

virtual

void
DisplayData()
{
}


///

<summary>


///
检查输入的有效性

///

</summary>


///

<returns>
有效
</returns>



public

virtual

bool
CheckInput()
{

return

true
;
}


///

<summary>


///
清除屏幕

///

</summary>



public

virtual

void
ClearScreen()
{

this
.ID
=

""
;
///
/需要设置为空,表示新增


ClearControlValue(
this
);

this
.FormOnLoad();
}


///

<summary>


///
保存数据(新增和编辑的保存)

///

</summary>



public

virtual

bool
SaveEntity()
{

bool
result
=

false
;

if
(
!
string
.IsNullOrEmpty(ID))
{

//
编辑的保存


result
=
SaveUpdated();
}

else

{

//
新增的保存


result
=
SaveAddNew();
}


return
result;
}


///

<summary>


///
更新已有的数据

///

</summary>


///

<returns></returns>



public

virtual

bool
SaveUpdated()
{

return

true
;
}


///

<summary>


///
保存新增的数据

///

</summary>


///

<returns></returns>



public

virtual

bool
SaveAddNew()
{

return

true
;
}


///

<summary>


///
保存

///

</summary>


///

<param name="close">
关闭窗体
</param>



private

void
SaveEntity(
bool
close)
{

//
检查输入的有效性



if
(
this
.CheckInput())
{

//
设置鼠标繁忙状态



this
.Cursor
=
Cursors.WaitCursor;

try

{

if
(
this
.SaveEntity())
{
MessageUtil.ShowTips(

"
保存成功
"
);

if
(close)
{

this
.DialogResult
=
DialogResult.OK;

this
.Close();
}

else

{

this
.ClearScreen();
}
}
}

catch
(Exception ex)
{

this
.ProcessException(ex);
}

finally

{

//
设置鼠标默认状态



this
.Cursor
=
Cursors.Default;
}
}
}


private

void
btnAdd_Click(
object
sender, EventArgs e)
{

this
.SaveEntity(
false
);
}


private

void
btnOK_Click(
object
sender, EventArgs e)
{

this
.SaveEntity(
true
);
}


private

void
btnCancel_Click(
object
sender, EventArgs e)
{

this
.DialogResult
=
DialogResult.Cancel;

this
.Close();
}

其中值得介绍的是,窗体的控件数据清空在基类窗体中通过遍历实现了通用的数据清空操作,该部分代码引用了“潇湘隐者的博客(
http://www.cnblogs.com/kerrycode/archive/2010/02/05/1664267.html
),对此感谢。

另外,基类窗体还实现了Tab键的转换,这个思路来源于即日嘎拉的代码,但由于是基类实现,有些不同,他的放在每个具体的子类中,因此通用性有些限制。

我们重载了ProcessCmdKey(ref Message msg, Keys keyData) 函数后,就可以实现统一的回车键转换了。



代码


protected

override

bool
ProcessCmdKey(
ref
Message msg, Keys keyData)
{

if
(keyData
==
Keys.F5)
{

this
.FormOnLoad();
}


if
((
!
(ActiveControl
is
Button))
&&

(keyData

==
Keys.Up
||
keyData
==
Keys.Down
||
keyData
==
Keys.Enter))
{

if
(keyData
==
Keys.Enter)
{
System.Windows.Forms.SendKeys.Send(

"
{TAB}
"
);

return

true
;
}

if
(keyData
==
Keys.Down)
{
System.Windows.Forms.SendKeys.Send(

"
{TAB}
"
);
}

else

{
SendKeys.Send(

"
+{Tab}
"
);
}


return

true
;
}

else

{

return

base
.ProcessCmdKey(
ref
msg, keyData);
}
}

最后,我们看看具体的子类窗体,看新增编辑界面需要实现的代码,如下所示,其中大部分是原子级别的操作,逻辑操作已经在基类中实现了哦:


代码


public

partial

class
FrmEditCustomer : BaseUI.BaseEditForm
{

public
FrmEditCustomer()
{
InitializeComponent();
}


///

<summary>


///
实现控件输入检查的函数

///

</summary>


///

<returns></returns>



public

override

bool
CheckInput()
{

bool
result
=

true
;


#region
输入验证


if
(
this
.txtName.Text.Length
==

0
)
{
MessageUtil.ShowTips(

"
宾客名称不能为空
"
);

this
.txtName.Focus();
result

=

false
;
}

else

if
(
this
.txtIDNumber.Text.Length
==

0
)
{
MessageUtil.ShowTips(

"
证件号码不能为空
"
);

this
.txtIDNumber.Focus();
result

=

false
;
}


#endregion



return
result;
}


///

<summary>


///
编辑或者保存状态下取值函数

///

</summary>


///

<param name="info"></param>



private

void
SetInfo(CustomerInfo info)
{
info.Address

=
txtAddress.Text;
info.CompanyName

=
txtCompany.Text;
info.IDCarType

=
cmbIDCarType.Text;
info.Name

=
txtName.Text;
info.Note

=
txtNote.Text;
info.IDNumber

=
txtIDNumber.Text;
info.Telephone

=
txtTelephone.Text;
info.Source

=
cmbSource.Text;
info.CustomerType

=
cmbType.Text;
info.Sex

=
cmbSex.Text;
}


///

<summary>


///
数据字典加载

///

</summary>



private

void
InitDictItem()
{

this
.cmbSource.Items.Clear();

this
.cmbSource.Items.AddRange(DictItemUtil.GetCustomerSource());


this
.cmbType.Items.Clear();

this
.cmbType.Items.AddRange(DictItemUtil.GetCustomerType());


this
.cmbIDCarType.Items.Clear();

this
.cmbIDCarType.Items.AddRange(DictItemUtil.GetIDCarType());
}


///

<summary>


///
数据显示的函数

///

</summary>



public

override

void
DisplayData()
{

//
数据字典加载(公用)


InitDictItem();


if
(
!
string
.IsNullOrEmpty(ID))
{

//
编辑状态下的数据显示


CustomerInfo info
=
BLLFactory
<
Customer
>
.Instance.FindByID(ID);

if
(info
!=

null
)
{

#region
显示客户信息

txtAddress.Text

=
info.Address;
txtCompany.Text

=
info.CompanyName;
txtName.Text

=
info.Name;
txtNote.Text

=
info.Note;
txtIDNumber.Text

=
info.IDNumber;
txtTelephone.Text

=
info.Telephone;
cmbSource.Text

=
info.Source;
cmbType.Text

=
info.CustomerType;
cmbSex.Text

=
info.Sex;
cmbIDCarType.Text

=
info.IDCarType;
lblCreateDate.Text

=
info.RegistrationDate.ToString();


#endregion

}
}

else

{

//
新增状态的数据显示


lblCreateDate.Text
=
DateTime.Now.ToString();
}
}


///

<summary>


///
新增状态下的数据保存

///

</summary>


///

<returns></returns>



public

override

bool
SaveAddNew()
{
CustomerInfo info

=

new
CustomerInfo();
SetInfo(info);
info.RegistrationDate

=
DateTime.Now;

bool
succeed
=
BLLFactory
<
Customer
>
.Instance.Insert(info);

return
succeed;
}


///

<summary>


///
编辑状态下的数据保存

///

</summary>


///

<returns></returns>



public

override

bool
SaveUpdated()
{
CustomerInfo info

=
BLLFactory
<
Customer
>
.Instance.FindByID(ID);

if
(info
!=

null
)
{
SetInfo(info);

bool
succeed
=
BLLFactory
<
Customer
>
.Instance.Update(info, info.ID.ToString());

return
succeed;
}

return

false
;
}
}

由于一朋友需要Visio实现仿真模拟Demo,因此我在原来Visio项目代码基础上进行改进调整,完成了其需要的功能。现将对原有Visio二次开发项目的创新思路及成果总结一下,以飨读者,也做为一个标志,供日后参考,或者于各位同行,一起研究切磋。先贴图展示整个项目的该款,表明此文章所托并非空穴来风、纸上谈兵!

本人基于项目的Visio的二次开发文章写了很多,其中涉及到各形状方面的操作,但基本上都是做为一个图形的整体来实现各种效果的,由于仿真需要,需要将换向阀实现左右变换功能,而换向阀是一个组合控件,基本上对组合控件操作都是一个整体性的操作,不能局部进行位置变换的,所以要实现这种变化,还真是费了不少功夫。

需求是要求做线路分析的时候,油经过后,组合图形实现效果变换,如下图所示:

上面的图形变化为下图(实现了左右组合图形的互换变化的效果)

一般光靠操作整个图形实现变化是不太可能的,因此考虑化整为零,把其中两个组合图形从整体中分离,然后再进行组合操作即可。首先尝试通过宏记录方式查看代码,得到分解后的形状操作宏代码如下所示:

Sub
Macro1()


Dim
UndoScopeID1
As

Long

UndoScopeID1

=
Application.BeginUndoScope(
"
移动对象
"
)
ActiveWindow.DeselectAll
ActiveWindow.Select Application.ActiveWindow.Page.Shapes.ItemFromID(

60
), visSubSelect
Application.ActiveWindow.Selection.Move

0.405512
,
-
0
#
ActiveWindow.Selection.RemoveFromGroup
Application.EndUndoScope UndoScopeID1,

True


ActiveWindow.DeselectAll
ActiveWindow.Select Application.ActiveWindow.Page.Shapes.ItemFromID(

25
), visSubSelect
Application.ActiveWindow.Selection.Move

0.314961
,
-
0
#


Dim
UndoScopeID2
As

Long

UndoScopeID2

=
Application.BeginUndoScope(
"
移动对象
"
)
ActiveWindow.DeselectAll
ActiveWindow.Select Application.ActiveWindow.Page.Shapes.ItemFromID(

60
), visSelect
Application.ActiveWindow.Selection.Move

-
0.688976
,
0
#
ActiveWindow.DeselectAll
ActiveWindow.Select Application.ActiveWindow.Page.Shapes.ItemFromID(

22
), visSelect
ActiveWindow.Select Application.ActiveWindow.Page.Shapes.ItemFromID(

60
), visSelect
ActiveWindow.Selection.AddToGroup
Application.EndUndoScope UndoScopeID2,

True



End Sub

至此,完成万里长征第一步,也证实分解后的图形,可以通过移动位置的方式实现,虽然定位形状我们不能靠ActiveWindow.Page.Shapes.ItemFromID(60)来定位,但是我们可以通过埋在形状中的设备类型参数来实现形状的定位的(这里我在设计模具的时候,把两个形状的的设备类型属性分别设置为“阀A"和”阀B“)。

进一步分析整体组合形状的ShapeSheet参数发现,里面有一个很重要的参数,如下图所示:

F1查看其帮助说明,如下所示

因此可以看到,可以通过该属性的设置,来实现组合形状的分离操作,在图纸上实际操作一下,把0修改为1或者2,果然可以实现子形状的选择操作了。

建立一个操作函数,来实现选择模式的修改,如下所示:


///

<summary>


///
设置形状的选择属性

///

</summary>


///

<param name="targetShape">
指定的形状
</param>


///

<param name="selectMode">


///
0 仅选择组合形状。

///
1 首先选择组合形状

///
2 首先选择组合的组成部分

///

</param>


[CLSCompliant(
false
)]

public

static

void
SetGroupSelectMode(Visio.Shape targetShape,
int
selectMode)
{
targetShape.get_CellsSRC((

short
)VisSectionIndices.visSectionObject, (
short
)VisRowIndices.visRowGroup,
(

short
)VisCellIndices.visGroupSelectMode).FormulaU
=
selectMode.ToString();
}

本人实现了图形变化效果,截取项目部分代码,作为分析研究之用,如下所示:



代码


else

if
(equipType
==

"
4/2阀
"
)
{
VisioUtility.SetGroupSelectMode(shape,

2
);
//
改变模式,使之可以选择子形状



#region
MyRegion


foreach
(Shape subShape
in
shape.Shapes)
{

string
subEquipType
=
VisioUtility.GetShapeCellValue(subShape,
"
设备类型
"
);

if
(subEquipType
==

"
阀A
"

||
subEquipType
==

"
阀B
"
)
{
subShape.Text

=
subEquipType;

if
(intValue
!=

0
)
{

continue
;
}

try

{
VisWindow.DeselectAll();
VisWindow.Select(subShape, (

short
)VisSelectArgs.visSubSelect);

if
(subEquipType
==

"
阀A
"
)
{
VisWindow.Selection.Move(

0.393701
,
0
,
"
inches
"
);
//
移动到右边


}

else

if
(subEquipType
==

"
阀B
"
)
{
VisWindow.Selection.Move(

-
0.393701
,
-
0
,
"
inches
"
);
//
移动到左边


}

System.Windows.Forms.Application.DoEvents();
Thread.Sleep(

50
);
}

catch
(Exception ex)
{
VisWindow.DeselectAll();
}
}
}
VisioUtility.SetGroupSelectMode(shape,

0
);
//
还原选择模式,成为组合形状



#endregion

}

世界本简单,只因道未知;无师可自通,码界无秘密。

Visio二次开发可以实现的项目情景很多,如电气线路分析、配电网络分析、流程图等,现因为项目需要,又认识多了一个应用场合,液压传动的仿真。项目效果图如下所示:

动态仿真,其实也就是模拟实际线路的走向,实现动画的展现。以前我的Visio的项目,基本上都是基于静态的图形展现,并没有设置太多的动态展现。原来配电网络的通电线路的分析,严格来说也是静态的,因为基本上是一次性把通电和不通电的线路给绘制出来。而动态仿真则要求慢慢的动画展现线路的走向和颜色变化。

如活塞运动的仿真,要求不停止动画的情况下,可以一直循环的变化。 如下图所示的效果:

本文介绍如何实现线路走向、颜色变化,以及特定图形(如活塞)的动态仿真效果。

首先实现动态仿真效果,必须先分析出整个图纸的拓扑网络顺序及层次,这样我们才能知道正确的线路走向以及动画的变化顺序,如配电网络线路图中,必定是电源开始,通过导线或者设备传递电源,以实现电路的贯通。在液压线路中,由油箱开始,经过一系列设备,最后又回到油箱。

要在Visio图纸上实现如上图的动画效果,其中最重要的奥秘是使用下面代码:

System.Windows.Forms.Application.DoEvents();
Thread.Sleep(

50
);

很多情况下,我们可能对这个DoEvents函数的功能不是很熟悉,其实我们可以理解为主动触发事件,让消息流提前进入处理流程,这样我们就能够看到在Visio图纸上的图形更新效果了。

整个图形分析的过程,分为3个步骤:

1)进行简单的拓扑分析,把设备周边的关系保持到数据库进行分析。

2)根据数据库结构,对设备关系进行分析,获得拓扑网络的设备层次结构列表

3)根据不同的设备类型和图纸当前状态,对设备进行适当的绘制和动画仿真展示。

大致的代码如下所示:


private

void
PowerCutAnalyze(Visio.Application app)
{

#region
获取操作设备和判断是否图纸有设备

Visio.Shape shapeSelected

=

null
;

try

{
Visio.Window wndVisio

=
app.ActiveWindow;

if
(wndVisio.Selection.Count
==

1
)
{
shapeSelected

=
wndVisio.Selection.get_Item16(
1
);
}
}

catch
{ ; }


if
(
!
VisioUtility.HasShapeInWindow(VisWindow))
{
MessageUtil.ShowWarning(

"
图纸上没有设备, 不能执行该操作
"
);

return
;
}

#endregion


app.UndoEnabled

=

false
;
List

<
string
>
list
=

new
List
<
string
>
();

string
message
=

""
;
list

=
powerCutBLL.RunPowerCutAnalyzing(app, shapeSelected,
ref
message);
app.UndoEnabled

=

true
;


if
(message
!=

""
)
{
MessageUtil.ShowError(message);

return
;
}


if
(list.Count
>

0
)
{
AnalyzeShapeIdList.Clear();

foreach
(
string
shapeStrID
in
list)
{
AnalyzeShapeIdList.Add(Convert.ToInt32(shapeStrID));
}
RunColorChanging(app);
}

else

{
MessageUtil.ShowWarning(

"
请检查线路是否连接正确。
"
);
}
}

线路颜色变化以及动画展示部分的代码如下所示 :


///

<summary>


///
根据分析后的设备ID,把设备变色动画展示

///

</summary>


///

<param name="visApp"></param>



private

void
RunColorChanging(Visio.Application visApp)
{
Visio.Cell cell

=
visApp.ActiveDocument.Pages[
1
].PageSheet.get_Cells(
"
Scratch.A1
"
);

int
intValue
=
Convert.ToInt32(VisioUtility.FormulaStringToString(cell.Formula));

if
(intValue
==

1
)
{
cell.Formula

=

"
0
"
;
}

else

{
cell.Formula

=

"
1
"
;
isMovie

=

!
isMovie;
}
...................


int
sequence
=

1
;

foreach
(
int
shapeId
in
AnalyzeShapeIdList)
{
Visio.Shape shape

=
VisDocument.Pages[
1
].Shapes.get_ItemFromID(shapeId);

if
(shape
!=

null
)
{

if
(intValue
==

0
)
{
shape.Text

=
sequence
++
.ToString(
"
D2
"
);
//
string.Format("{0}({1})", sequence++, shape.ID);
//


VisioUtility.SetShapeLineColor(shape, VisDefaultColors.visDarkGreen);

//
有电(绿色)


System.Windows.Forms.Application.DoEvents();
Thread.Sleep(

500

*
minFlowValue);
}

else

{
shape.Text

=

""
;
VisioUtility.SetShapeLineColor(shape, VisDefaultColors.visBlack);

//
无电(黑色)


System.Windows.Forms.Application.DoEvents();
}


string
equipType
=
VisioUtility.GetShapeCellValue(shape,
"
设备类型
"
);

if
(
!
string
.IsNullOrEmpty(equipType))
{

#region
单作用、双作用


if
(equipType
==

"
单作用
"

||
equipType
==

"
双作用
"
)
{

string
minValue
=

"
Width*0.25
"
;

string
maxValue
=

"
Width*0.75
"
;

string
cellName
=

"
Controls.Row_1.X
"
;

try

{

if
(shape.get_CellExistsU(cellName, (
short
)VisExistsFlags.visExistsAnywhere)
!=

0
)
{

short
i
=
shape.get_CellsRowIndex(cellName);
Visio.Cell typeCell

=
shape.get_CellsSRC((
short
)VisSectionIndices.visSectionControls, i, (
short
)VisCellIndices.visCtlX);

if
(intValue
==

0
)
{
ThreadParameterInfo param

=

new
ThreadParameterInfo();
param.Cell

=
typeCell;
param.ScratchCell

=
cell;

Thread thread

=

new
Thread(
new
ParameterizedThreadStart(HuoSaiMoving));
thread.Start(param);
}

else

{
typeCell.Formula

=
VisioUtility.StringToFormulaForString(minValue);
System.Windows.Forms.Application.DoEvents();

//
Thread.Sleep(500 * minFlowValue);


}
}
}

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

#endregion

}
}

}
}

其中我们注意到了,活塞运动时一个独立的线程进行处理的,如下所示

Thread thread
=

new
Thread(
new
ParameterizedThreadStart(HuoSaiMoving));
thread.Start(param);

活塞运动是在线路联通后,继续循环进行动画的展示的,因为它是独立一个线程进行处理操作,通过判断标识来实现动画的停止控制的,具体处理活塞动画的效果实现代码如下所示:



代码


private

void
HuoSaiMoving(
object
obj)
{
ThreadParameterInfo objParam

=
obj
as
ThreadParameterInfo;
Visio.Cell scratchCell

=
objParam.ScratchCell;
Visio.Cell typeCell

=
objParam.Cell;

int
intValue
=
Convert.ToInt32(VisioUtility.FormulaStringToString(scratchCell.Formula));

while
(intValue
==

1

&&
isMovie)
{

string
minValue
=

"
Width*0.25
"
;

string
maxValue
=

"
Width*0.75
"
;

//
Visio.Cell typeCell = objCell as Visio.Cell;



if
(typeCell
!=

null
)
{

string
currentValue
=

""
;

//
增加



for
(
int
k
=

1
; k
<=

10
; k
++
)
{
currentValue

=

string
.Format(
"
Width*0.25 + Width*{0}
"
,
0.05

*
k);
typeCell.Formula

=
VisioUtility.StringToFormulaForString(currentValue);
System.Windows.Forms.Application.DoEvents();
Thread.Sleep(

50
);
}

//
减少



for
(
int
k
=

1
; k
<=

10
; k
++
)
{
currentValue

=

string
.Format(
"
Width*0.75 - Width*{0}
"
,
0.05

*
k);
typeCell.Formula

=
VisioUtility.StringToFormulaForString(currentValue);
System.Windows.Forms.Application.DoEvents();
Thread.Sleep(

50
);
}
}
intValue

=
Convert.ToInt32(VisioUtility.FormulaStringToString(scratchCell.Formula));
}
}

Visio应用曲高和寡,代码贴图众口难调;不求一鸣惊人,但求潜移默化。

严格来讲,本文不是一个技术贴,但是却和我们的技术探索息息相关,每个人的寻找资源的习惯不同,因此获得的资源类型及数量也不同。至于如何获取相应的资源,各种习惯和方法,个人有个人的选择,本无可厚非,这里仅仅是从我本人的习惯方法,来谈论下获取资源的思路及途径,抛砖引玉,共同受益。

作为技术人员来说,我们一般常用的查找资源途径,基本上是通过Google、Bing、Baidu的查询,有时候也会跟着去Codeproject、博客园、CSDN等其他地方去寻找所要的资源,这些途径基本上能满足大多数的期望值,但是有时候针对性不够强,需要经过多次的筛选,才能寻到需要的代码或者控件等。

其实作为代码的查找,有一个地方
http://www.koders.com/
,这个是很不错的寻找代码地方,它里面有大量的开源代码,顺藤摸瓜,追本溯源,我们就可以找到优秀开源组件的地址,或下载,或借鉴,善莫大焉。

例如,有一天,我突然有一个想法,想做一个POP3相关的程序,因此在Google上搜索了一下POP3 C#,发现很多基本的介绍、概念的陈述,翻了多页也未获得我期望的东西,因此期望有更好的面向开发的代码搜索,因此想到了
http://www.koders.com/

如我查找了PoP、C#语言的内容,得到下面的搜索结果,其中很多可以参考人家的一些代码思路,有些可以直接下载过来用的。


很快可以找到一个如下所示的代码片段

当看中一些比较相似的结果后,我们就可以看其完整的项目地址,从中找到开源组件资源的下载地址即可。

这样我们在上图红色框框中就可以项目的完整地址,然后到具体的首页去下载所需要的资源即可,绝大多数的项目源码是可以下载的。

上面这个组件就是非常有名的
LumiSoft
.
Net
开源组件,这样你再在Google、百度、Bing上进行搜索,就知道该组件的具体用法和更详细的资源了。非常棒吧,这样你就可以利用前人弄好的轮子(而且是性能非常不错的轮子),直接装配使用即可,不需要再去做笨龊的重复发明了。

采用了该组件的POP部分组件,测试下用POP方式获取邮件,代码变得非常方便和简单明了,代码如下所示:

List
<
string
>
gotEmailIds
=

new
List
<
string
>
();
List

<
Mime
>
result
=

new
List
<
Mime
>
();

using
(POP3_Client pop3
=

new
POP3_Client())
{
pop3.Connect(

"
pop.163.com
"
,
110
);
pop3.Authenticate(

"
wuhuacong
"
,
"
******
"
,
false
);
POP3_ClientMessageCollection infos

=
pop3.Messages;


int
i
=

0
;

foreach
(POP3_ClientMessage info
in
infos)
{

//
每封Email会有一个在Pop3服务器范围内唯一的Id,检查这个Id是否存在就可以知道以前有没有接收过这封邮件



if
(gotEmailIds.Contains(info.UID))

continue
;

//
获取这封邮件的内容



byte
[] bytes
=
info.MessageToByte();

//
记录这封邮件的Id


gotEmailIds.Add(info.UID);

//
解析从Pop3服务器发送过来的邮件信息



LumiSoft.Net.Mail.Mail_Message msg

=
LumiSoft.Net.Mail.Mail_Message.ParseFromByte(bytes);
Console.WriteLine(msg.BodyText);

if
(msg.Date.AddDays(
5
)
<
DateTime.Now)
{
Console.ReadLine();

break
;
}
}
}

性能非常不错,而且获取到的邮件内容不会出现乱码,比直接在Google上找到的代码例子好很多,那些或多或少只是一个试验品,和这样的优秀组件的质量差别还是相当大的。

本人以上应用纯粹只是一个日常例子,但是很多情况下,我们如果能够增加这样的步骤去寻找一些东西,获得的收益是无穷的,

上天不能给你的东西,Google可以帮到你,Google帮不到你的地方,可以试试koders。