2007年03月27日

响应KeyPress事件

    /*全角字符从的unicode编码从65281~65374   
      半角字符从的unicode编码从               33~126   
     * 差值65248
      空格比较特殊,全角为       12288,半角为       32 
     
*/
    
public char FullCodeToHalfCode(char c)
    {
        
//得到c的编码
        byte[] bytes = System.Text.Encoding.Unicode.GetBytes(c.ToString());

        int H = Convert.ToInt32(bytes[1]);
        
int L = Convert.ToInt32(bytes[0]);

        //得到unicode编码
        int value = H * 256 + L;

        //是全角
        if (value >= 65281 && value <= 65374)
        {
            
int halfvalue = value - 65248;//65248是全半角间的差值。
            byte halfL = Convert.ToByte(halfvalue);

            bytes[0= halfL;
            bytes[
1= 0;
        }
        
else if (value == 12288)
        {
            
int halfvalue = 32;
            
byte halfL = Convert.ToByte(halfvalue);

            bytes[0= halfL;
            bytes[
1= 0;
        }
        
else
        {
            
return c;
        }

        //将bytes转换成字符
        string ret = System.Text.Encoding.Unicode.GetString(bytes);

        return Convert.ToChar(ret);
    }

 

C#中文乱码解决:UTF8 转 UNICODE

XML文件可以采用多种编码,但是经过不同的编码后对于中文会出现乱码问题,比如“骞垮憡涓戦椈”,对于此问题的解决如下:

static void Main()
      {
         string utf8String = "骞垮憡涓戦椈";

         // Create two different encodings.
         Encoding utf8= Encoding.UTF8;
         Encoding defaultCode= Encoding.Default;

         // Convert the string into a byte[].
         byte[] utf8Bytes = default.GetBytes(utf8String );

         // Perform the conversion from one encoding to the other.
         byte[] defaultBytes = Encoding.Convert(utf8, defaultCode, utf8Bytes );
           
         // Convert the new byte[] into a char[] and then into a string.
         // This is a slightly different approach to converting to illustrate
         // the use of GetCharCount/GetChars.
         char[] defaultChars = new char[defaultCode.GetCharCount(defaultBytes , 0, defaultBytes .Length)];
         defaultCode.GetChars(defaultBytes , 0, defaultBytes .Length, defaultChars , 0);
         string defaultString = new string(defaultChars );

         // Display the strings created before and after the conversion.
         Console.WriteLine("Original string: {0}", utf8String);
         Console.WriteLine("Ascii converted string: {0}", defaultString);

//或者如下:
         byte[] buffer1 = Encoding.Default.GetBytes(utf8String );
         byte[] buffer2 = Encoding.Convert(Encoding.UTF8, Encoding.Default, buffer1, 0, buffer1.Length);
         string strBuffer = Encoding.Default.GetString(buffer2, 0, buffer2.Length);
      }

2007年03月14日

浅谈C#托管程序中的资源释放问题

便于对文章的开展,需要先明确两个概念。
第一个就是很多人用.Net写程序,会谈到托管这个概念。那么.Net所指的资源托管到底是什么意思,是相对于所有资源,还是只限于某一方面资源?很多人对此不是很了解,其实.Net所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于Stream,数据库的连接,GDI+的相关对象,还有Com对象等等,这些资源并不是受到.Net管理而统称为非托管资源。而对于内存的释放和回收,系统提供了GC-Garbage Collector,而至于其他资源则需要手动进行释放。

那么第二个概念就是什么是垃圾,通过我以前的文章,会了解到.Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要GC回收;后者是分配在堆上,因此它的内存释放和回收需要通过GC来完成。GC的全称为“Garbage Collector,顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被GC回收。也就是说,一个引用类型对象所占用的内存需要被GC回收,需要先成为垃圾。那么.Net如何判定一个引用类型对象是垃圾呢,.Net的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。

明确了这两个基本概念,接下来说说GC的运作方式以及其的功能。内存的释放和回收需要伴随着程序的运行,因此系统为GC安排了独立的线程。那么GC的工作大致是,查询内存中对象是否成为垃圾,然后对垃圾进行释放和回收。那么对于GC对于内存回收采取了一定的优先算法进行轮循回收内存资源。其次,对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。GC对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。

很明显得知,对于某个具体的资源,无法确切知道,对象析构函数什么时候被调用,以及GC什么时候会去释放和回收它所占用的内存。那么对于从CC++之类语言转换过来的程序员来说,这里需要转变观念。

那么对于程序资源来说,我们应该做些什么,以及如何去做,才能使程序效率最高,同时占用资源能尽快的释放。前面也说了,资源分为两种,托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了;那么对于非托管的资源,这里再重申一下,就是Stream,数据库的连接,GDI+的相关对象,还有Com对象等等这些资源,需要我们手动去释放。

如何去释放,应该把这些操作放到哪里比较好呢。.Net提供了三种方法,也是最常见的三种,大致如下:
<!–[if !supportLists]–>1.  <!–[endif]–>析构函数;
<!–[if !supportLists]–>2.  <!–[endif]–>继承IDisposable接口,实现Dispose方法;
<!–[if !supportLists]–>3.  <!–[endif]–>提供Close方法。

经过前面的介绍,可以知道析构函数只能被GC来调用的,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄漏,毕竟它会被GC调用,因此析构函数可以作为一个补救方法。而CloseDispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。例如,常见SqlConnection这个类,当调用完Close方法后,可以通过Open重新打开数据库连接,当彻底不用这个对象了就可以调用Dispose方法来标记此对象无用,等待GC回收。明白了这两种方法的意思后,大家在往自己的类中添加的接口时候,不要歪曲了这两者意思。

接下来说说这三个函数的调用时机,我用几个试验结果来进行说明,可能会使大家的印象更深。
首先是这三种方法的实现,大致如下:

    ///<summary>

    /// The class to show three disposal function

    ///</summary>

    public class DisposeClass:IDisposable

    {

        public void Close()

        {

            Debug.WriteLine( "Close called!" );

        }

        ~DisposeClass()

        {

            Debug.WriteLine( "Destructor called!" );

        }

        #region IDisposable Members

        public void Dispose()

        {

            // TODO:  Add DisposeClass.Dispose implementation

            Debug.WriteLine( "Dispose called!" );

        }

        #endregion

    }

对于Close来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用GC.Collect方法的时候才被调用,参看如下的代码运行结果。

        private void Create()

        {

            DisposeClass myClass = new DisposeClass();

        }

        private void CallGC()

        {

            GC.Collect();

        }

        // Show destructor

        Create();

        Debug.WriteLine( "After created!" );

        CallGC();

运行的结果为:

After created!

Destructor called!

显然在出了Create函数外,myClass对象的析构函数没有被立刻调用,而是等显示调用GC.Collect才被调用。

对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。例如:

    using( DisposeClass myClass = new DisposeClass() )

    {

        //other operation here

    }

如上运行的结果如下:
Dispose called!

那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,按照前面的GC流程来说,GC对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的Dispose函数,虽说被执行了,但是GC还是需要执行析构函数,那么一个完整的Dispose函数,应该通过调用GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。那么改写后的DisposeClass如下:

    ///<summary>

    /// The class to show three disposal function

    ///</summary>

    public class DisposeClass:IDisposable

    {

        public void Close()

        {

            Debug.WriteLine( "Close called!" );

        }

        ~DisposeClass()

        {

            Debug.WriteLine( "Destructor called!" );

        }

        #region IDisposable Members

        public void Dispose()

        {

            // TODO:  Add DisposeClass.Dispose implementation

            Debug.WriteLine( "Dispose called!" );

            GC.SuppressFinalize( this );

        }

        #endregion

    }

通过如下的代码进行测试。

        private void Run()

        {

            using( DisposeClass myClass = new DisposeClass() )

            {

                //other operation here

            }

        }

        private void CallGC()

        {

            GC.Collect();

        }

        // Show destructor

        Run();

        Debug.WriteLine( "After Run!" );

        CallGC();

运行的结果如下:

Dispose called!

After Run!

显然对象的析构函数没有被调用。通过如上的实验以及文字说明,大家会得到如下的一个对比表格。

析构函数

Dispose方法

Close方法

意义

销毁对象 销毁对象 关闭对象资源

调用方式

不能被显示调用,会被GC调用 需要显示调用
或者通过using语句
需要显示调用

调用时机

不确定 确定,在显示调用或者离开using程序块 确定,在显示调用时

那么在定义一个类型的时候,是否一定要给出这三个函数地实现呢。

我的建议大致如下。
<!–[if !supportLists]–>1.<!–[endif]–>提供析构函数,避免资源未被释放,主要是指非内存资源;
<!–[if !supportLists]–>2.<!–[endif]–>对于DisposeClose方法来说,需要看所定义的类型所使用的资源(参看前面所说),而决定是否去定义这两个函数;
<!–[if !supportLists]–>3.<!–[endif]–>在实现Dispose方法的时候,一定要加上“GC.SuppressFinalize( this )”语句,避免再让GC调用对象的析构函数。

C#程序所使用的内存是受托管的,但不意味着滥用,好地编程习惯有利于提高代码的质量以及程序的运行效率。

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1023352

2007年03月13日

WMI 的一个实现

作者:Paul Li

翻译:Abbey



原文出处:Code Project:Windows Management Instrumentation (WMI) Implementation

源代码下载:wmi.zip(45KB)


介绍

  这是我在继上一篇文章"My Explorer"之后关于Windows Management Instrumentation(Windows管理规范)的又一新作。我将向你展示一些技巧,让你可以在远程地访问网络中其他计算机的操作系统、服务、当前运行着的进程等等信息,当然前提是你必须得拥有这些计算机的管理员权限。同时我也将向你展示如何利用WMI来启动或者停止服务、终止进程、创建进程。这是程序的主界面:

                      

开始

  在这个WMI应用程序里,我创建了一个包含了四个用户控制的库WMIControlLibrary。这四个用户控制分别是Explorer,SystemInfo,Services与Processes。每个控制都有其特定的功用。以下是对每个控制作用的一个简单描述:

  • Explorer控制     我把我那个"My Explorer"转换成了一个用户控制,它还是用来显示你系统上的驱动器、目录、文件等信息。
  • SystemInfo 控制* 这个控制用来显示操作系统与硬件数据及清单等信息。
  • Services 控制*   这个控制用来显示系统当前运行着的服务。
  • Process 控制*    这个控制用来显示系统当前运行着的进程。

(*注意:这个控制可以用来监控本地或者网络上的远程系统。)

上述的每个控制都引用了System.Management命名空间,以保证它们能访问各自特定的系统信息。

控制的状态事件

  这其中的一些控制需要点时间才能从系统获取相关的信息,因此我在每个控制中都实现了一个事件UpdateStatus(string e)。这样每个控制就可以更新主应用程序窗体的状态条,用户也能很清楚地知道控制正在干什么了。

//控制内部的代码
//声明一个Status的事件委托类型
public delegate void Status(string e);

//声明了一个更新状态的事件

public event Status UpdateStatus;

//更新状态条
UpdateStatus("Hello world.");

//主程序代码
//用参数中的字符串刷新状态条的显示文本
private void refreshStatusBar(string stringStatus)
{
    //更新状态条
    statusBarStatus.Text = stringStatus;
}

Explorer 控制

  在Explorer控制内部,我使用了WMI的Win32_LogicalDisk类来访问所有本地的及网络映射的驱动器。要访问驱动器的相关信息,我得先使用一个ManagementObjectSearcher对象来获取一个包含了我所需驱动器信息的ManagementOjbectCollection对象(译注:原文用的是class,我认为不准确,因此改译为对象)。之后,我们就可以自由支配所有这些驱动器的信息。(比如驱动器名、类型、卷标、标识等等)。你也可以只查询剩余空间低于1MByte的驱动器的信息,对此只需要改变ManagementObjectSearcher参数而已:

//译注:这句就是查询剩余空间低于1MByte的SQL语句,用在ManagementObjectSearcher的构造时。
//是不是很象一般数据库编程里用的SQL语句啊?
Select * From Win32_LogicalDisk Where FreeSpace < 1000000

//取得驱动器集
ManagementObjectSearcher query =                        new ManagementObjectSearcher ("SELECT * From Win32_LogicalDisk "); 

ManagementObjectCollection queryCollection = query.Get(); 

//遍历每个对象,以获取每个驱动器的信息
foreach ( ManagementObject mo in queryCollection)
{
    switch (int.Parse( mo["DriveType"].ToString()))
    {
        case Removable: //可移动驱动器
            imageIndex = 5;
            selectIndex = 5;
            break; 

        case LocalDisk: //本地驱动器
            imageIndex = 6;
            selectIndex = 6;
            break; 

        case CD: //CD-ROM驱动器
            imageIndex = 7;
            selectIndex = 7;
            break; 

        case Network: //网络驱动器
            imageIndex = 8;
            selectIndex = 8;
            break; 

        default: //缺省:文件夹
            imageIndex = 2;
            selectIndex = 3;
            break;
    } 

    //获取驱动器名
    Console.WriteLine("Drive: " + mo["Name"].ToString());
} 

SystemInfo 控制

  SystemInfo控制用于显示你的本地计算机或者远程计算机上一些不同类型的信息。它首先定义一个ConnectionOptions对象,并设置好该对象的UserName与Password属性,准备用此来建立一个WMI的连接。之后再以该ConnectionOptions对象为参数,使用本地或远程计算机的主机名创建一个ManagementScope对象。

//建立远程计算机连接
ConnectionOptions co = new ConnectionOptions();

co.Username = textUserID.Text;
co.Password = textPassword.Text;

//将管理范围确定为该远程计算机
System.Management.ManagementScope ms = new System.Management.ManagementScope
					("\\\\" + stringHostName + "\\root\\cimv2", co); 

  现在我们就要准备通过创建一个ObjectQuery 成员对象来访问这个系统上的信息了。我们需要利用这个ObjectQuery对象和之前的那个ManagementScope对象来创建一个ManagementObjectSearcher对象。然后再调用该ManagementObjectSearcher对象的Get()方法来执行ObjectQuery对象定义的那个查询命令,并将查询结果返回到一个ManagementObject对象集中。

//查询操作系统信息
oq = new System.Management.ObjectQuery("SELECT * FROM Win32_OperatingSystem");

query = new ManagementObjectSearcher(ms,oq);

queryCollection = query.Get();

foreach ( ManagementObject mo in queryCollection)
{
    //在树中创建一个操作系统的子结点
    createChildNode(nodeCollection, "Operating System: " + mo["Caption"]);
    createChildNode(nodeCollection, "Version: " + mo["Version"]);
    createChildNode(nodeCollection, "Manufacturer : " + mo["Manufacturer"]);
    createChildNode(nodeCollection, "Computer Name : " + mo["csname"]);
    createChildNode(nodeCollection, "Windows Directory : " + mo["WindowsDirectory"]);
}

  如果你只关心本地主机的信息,那你可以不用创建ConnectionOption,ManagementScope,与ObjectQuery这些对象。你只需要用SQL查询语句串创建一个ManagementObjectSearcher对象,然后直接调用该对象的Get()方法,就能以一个ManagementObjectCollection对象的形式返回本地主机的信息了。

ManagementObjectSearcher query = new ManagementObjectSearcher("SELECT * From Win32_OperatingSystem");
ManagementObjectCollection queryCollection = query.Get();

  SystemInfo控制也用于显示计算机相关的其他信息:系统制造商,处理器,BIOS,时区,内存、网络连接、显卡等等。用于查询这些信息的代码只是在SQL查询语句和返回属性上不同而已,所以为了减少篇幅我就不把代码写出来了。具体的代码你可以看下载包里的内容。

Service 控制

Service控制使用了这样的一个查询来返回系统中所有服务的信息:

SELECT * FROM Win32_Service

  为了能启动或者停止一个服务,我为ListView动态地创建了一个弹出式菜单(上下文菜单)。你在列表的某个项上单击鼠标右键时,一个启动或停止服务(依赖于服务的当前运行状态)的菜单就会弹出。当菜单项被点击后,我需要利用这样的查询语句获得该服务的ManagementObject对象:

SELECT * FROM Win32_Service WHERE Name = ''ServiceName''

  接着我就可以通过调用ManagementObject.InvokeMethod()方法来启动或者停止该服务了。InvokeMethod()方法的第一个参数是一个Observer。我传递一个ManagementOperationObserver对象给这个方法,来管理这些异步操作,以及相应的异步事件与信息。通过检查返回的completionHandlerObj.ReturnObject的returnValue属性,我们就可以确定操作是否成功了。

/// 
/// List view的鼠标右击事件导致动态上下文菜单的生成
/// 

private void listViewServices_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
    System.Windows.Forms.ListView listViewObject = (System.Windows.Forms.ListView) sender;
    ContextMenu mnuContextMenu = new ContextMenu();
    MenuItem menuItem = new MenuItem();
    ManagementObjectCollection queryCollection;

    //是否是鼠标右键单击
    if (e.Button == System.Windows.Forms.MouseButtons.Right)
    {
        //取得服务的名称
        ServiceName = listViewObject.GetItemAt(e.X, e.Y).Text;

        //取得列表项
        ServiceItem = listViewObject.GetItemAt(e.X,e.Y);

        //创建弹出式菜单
        listViewObject.ContextMenu = mnuContextMenu;

        try
        {
            //取得特定的服务对象
            queryCollection = getServiceCollection("SELECT * FROM Win32_Service Where Name =
		''" + ServiceName + "''");
            foreach ( ManagementObject mo in queryCollection)
            {
                //据服务的当前状态创建相应的菜单
                if (mo["Started"].Equals(true))
                {
                    menuItem.Text = "Stop";

                    //设置动作Action属性
                    ServiceAction = "StopService";
                }
                else
                {
                    menuItem.Text = "Start";

                    ServiceAction = "StartService";
                }
                mnuContextMenu.MenuItems.Add(menuItem);

                // 给菜单项挂上事件处理函数
                menuItem.Click  += new System.EventHandler(this.menuItem_Click);
            }
        }
        catch (Exception e1)
        {
            MessageBox.Show("Error: " + e1);
        }
    }
}

/// 
/// List view上下文菜单的事件响应函数
///
///
///
private void menuItem_Click(object sender, System.EventArgs e)
{
    ManagementObjectCollection queryCollection;
    ListViewItem lvItem;

    //设置一个异步回调函数的句柄
    ManagementOperationObserver observer = new ManagementOperationObserver();
    completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
    observer.ObjectReady += new ObjectReadyEventHandler(completionHandlerObj.Done);

    //获得特定的服务对象
    queryCollection = getServiceCollection("Select * from Win32_Service Where Name =''" +
                           ServiceName + "''");

    //更新状态条
    updateStatus("Starting/Stopping service..."); 

    foreach ( ManagementObject mo in queryCollection)
    {
        //启动或者停止服务
        mo.InvokeMethod(observer, ServiceAction, null);
    }
    //等待,直到invoke调用完成或者超时5秒后 

    int intCount = 0;  

    while(!completionHandlerObj.IsComplete)
    {
        if
        (intCount >  10)
        {
            MessageBox.Show("Terminate process timed out.", "Terminate Process Status");
            break;
        }

        //等待0.5秒
        System.Threading.Thread.Sleep(500); 

        //增加计数器
        intCount++;
    } 

    //检查是否成功执行了
    if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
    {
        //成功
        lvItem = ServiceItem;

        if (ServiceAction == "StartService")
            lvItem.SubItems[2].Text = "Started";
        else
            lvItem.SubItems[2].Text = "Stop";
    }
    else
    {
        //错误信息
        string stringAction;

        if (ServiceAction == "StartService")
            stringAction = "start";
        else
            stringAction = "stop";

        MessageBox.Show("Failed to " + stringAction + " service " + ServiceName + ".",
                                        "Start/Stop Service Failure");
    }

    //清除对象
    ServiceName = "";
    ServiceAction = "";
    ServiceItem = null;

    //更新状态条
    updateStatus("Ready");
    this.Update();
}

//----------------------------------
// 完整的处理器
//----------------------------------
using System;
using System.Management;

namespace completionHandler
{
    /// 
    /// MyHandler类在InvokeMethod()调用完成后处理通知
    ///
    public class MyHandler
    {
        private bool isComplete = false;
        private ManagementBaseObject returnObject;

        /// 
        /// 当InvokeMethod完成后触发Done事件
        ///
        public void Done(object sender, ObjectReadyEventArgs e)
        {
            isComplete = true;
            returnObject = e.NewObject;
        }

        /// 
        /// 取IsComplete属性
        ///
        public bool IsComplete
        {
            get
            {
                return isComplete;
            }
        }

        /// 
        /// 属性允许访问主函数里的返回结果
        ///
        public ManagementBaseObject ReturnObject
        {
            get
            {
                return returnObject;
            }
        }

    }
}

Process 控制

  Process控制显示系统中运行着的进程,启动进程的用户,CPU使用率,内存的使用情况。要获得进程的用户信息,需要调用GetOwner(User, Domain)方法,其中的User 与Domain是传出参数。我们如何才能从InvokeMethod()调用中取回这些传出型参数呢?这实际取决于我们是如何实现这个InvokeMethod()方法的。如果我们不需要管理异步操作,那么我们只需要传递一个string数组给InvokeMethod()以获取传出的参数值。否则,我们就无需给InvokeMethod()传递任何的参数了,而是从completionHandlerObj.ReturnObject属性中取回传出的参数值。

//-------------------------------------------------
//在不使用observer对象的情况下获取进程用户信息
//--------------------------------------------------
//为InvokeMethod()方法准备参数表

string[] methodArgs = {"", ""}; 

//获取进程用户信息
mo.InvokeMethod("GetOwner", methodArgs); 

//methodArgs[0] 进程用户
//methodArgs[1] 进程的域 

//-----------------------------------------------
//在使用observer对象的情况下获取进程用户信息
//-----------------------------------------------
mo.InvokeMethod(observer,"GetOwner", null);

while (!completionHandlerObj.IsComplete)
{
    System.Threading.Thread.Sleep(500);
} 

if (completionHandlerObj.ReturnObject["returnValue"].
    ToString() == "0") 

    structProcess.stringUserName = completionHandlerObj.
        ReturnObject.Properties["User"].Value.ToString();
else
    structProcess.stringUserName = "";

终止进程

  终止一个特定的进程与启动或停止一个服务类似。首先还是要取得选中进程对应的 ManagementObject 对象,然后通过调用InvokeMethod(observer, "Terminate", null) 来杀死一个进程。

//设置一个异步回调的句柄
ManagementOperationObserver observer = new ManagementOperationObserver();
completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
observer.ObjectReady  += new ObjectReadyEventHandler(completionHandlerObj.Done);

//获取进程的ManagementObject
queryCollection = getProcessCollection("Select * from Win32_Process Where ProcessID =
				''" + ProcessID + "''");

//更新状态条
updateStatus("Invoking terminate process");

foreach ( ManagementObject mo in queryCollection)
{
    //启动或者停止服务(译注:作者真懒?)
    mo.InvokeMethod(observer, "Terminate", null);
}

//等待,直到invoke调用完成或者超时5秒后 

int intCount = 0;
while (!completionHandlerObj.IsComplete)
{
    if (intCount == 10)
    {
        MessageBox.Show("Terminate process timed out.", "Terminate Process Status");
        break;
    }
    //等待0.5秒
    System.Threading.Thread.Sleep(500); 

    //增加计数器
    intCount++;
} 

if (intCount != 10)
{
    //InvokeMethod尚未超时
    if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
    {
        lvItem = ProcessItem;
        lvItem.Remove();
    }
    else
    {
        MessageBox.Show("Error terminating process.",
            "Terminate Process");
    }
}

创建进程

  要创建一个进程,我们需要调用ManagementClass 的InvokeMethod ()方法。我们可以这么创建一个ManagementClass对象:

ManagementClass processClass = New ManagementClass(ms,path,null);

  其中的ms是一个ManagementScope对象,path是一个ManagementPath对象。ManagementScope对应了一个管理操作对应的范围。ManagementPath则提供了一个对Win32_Process进行解析与创建的封装。在调用ManagementClass.InvokeMethod(observer, methodName, inParameters)之前,我们还需要做点其他的准备。我们得把四个传入参数封装到一个object数组里。

uint32 Create(string CommandLine,
				string CurrentDirectory,
				Win32_ProcessStartup ProcessStartupInformation,
			    uint32* ProcessId);

参数说明

  • CommandLine – [传入] 要执行的命令行。如果有必要,系统会自动在末尾追加一个null字符来截断该串,表示真正要执行的文件。
  • CurrentDirectory – [传入] 子进程的当前驱动器与当前目录。这个串必须保证当前目录能解析到一个已知的路径。用户可以定义一个绝对的或相对的路径作为当前的工作目录。如果该参数为null,新创建的进程就会使用父进程的同一路径。这样做是主要是为了保证操作系统外壳能确定应用程序启动的初始驱动器和工作目录。
  • ProcessStartupInformation – [传入] 这是一个Windows进程的启动配置,请参见Win32_ProcessStartup.
  • ProcessId – [传出] 一个全局的用于标识进程的标识符。这个值的生存期自进程创建时起,至进程终结时止。
//为InvokeMethod()准备参数
object[] methodArgs = {stringCommandLine, null, null, 0};

//执行这个方法
processClass.InvokeMethod (observer, "Create", methodArgs);

  下面是创建进程的实现代码。我编写了一个CreateProcess()函数接受一个传入的命令行字符串stringCommandLine作为参数。当你调用CreateProcess("Calc.exe")时,就意味着创建了一个新的计算器的进程。就这么简单。

/// 
/// 在一个本地或者远程机器上调用Create方法
///
///
private void CreateProcess(string stringCommandLine)
{
    //设置一个异步回调的句柄
    ManagementOperationObserver observer = new ManagementOperationObserver();
    completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
    observer.ObjectReady  += new ObjectReadyEventHandler(completionHandlerObj.Done);

    string stringMachineName = "";

    //连接到远程计算机
    ConnectionOptions co = new ConnectionOptions();

    if (radioMachine.Checked == true)
    {
        stringMachineName = "localhost";
    }
    else
    {
        stringMachineName = textIP.Text;
    }

    if (stringMachineName.Trim().Length == 0)
    {
        MessageBox.Show("Must enter machine IP address or name.");
        return;
    }

    //获取用户名与密码
    if (textUserID.Text.Trim().Length   > 0)
    {
        co.Username = textUserID.Text;
        co.Password = textPassword.Text;
    }

    //获取指向机器的接入点
    System.Management.ManagementScope ms = new System.Management.ManagementScope("\\\\" +
	        stringMachineName + "\\root\\cimv2", co);      

  //获取进程的路径
    ManagementPath path = new ManagementPath( "Win32_Process");

    //获取将要被调用的进程的对象
    ManagementClass processClass = new ManagementClass(ms,path,null);

    //更新状态条
    updateStatus("Create process " + stringCommandLine + ".");

    //为方法准备参数
    object[] methodArgs = {stringCommandLine, null, null, 0};

    //执行方法
    processClass.InvokeMethod (observer, "Create", methodArgs);

    //等待,直到invoke调用完成或者超时5秒后
    int intCount = 0;
    while (!completionHandlerObj.IsComplete)
    {
        if (intCount > 10)
        {
            MessageBox.Show("Create process timed out.", "Terminate Process Status");
            break;
        }
         //等待0.5秒
        System.Threading.Thread.Sleep(500); 

         //增加计数器
        intCount++;
    } 

    if (intCount != 10)
    {
        //InvokeMethod尚未超时
        //检查是否出现错误
        if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
        {
            //刷新进程列表
            this.Refresh();
        }
        else
        {
            MessageBox.Show("Error creating new process.",
                "Create New Process");
        }
    }

    //更新状态条
    updateStatus("Ready");
    this.Update();
}

总结

  编写这个演示用的WMI应用程序,增加了我不少的经验。这只展示了WMI很小一部分的功能。我想有了我给出的注释,代码还容易理解吧。

你可以使用WMI完成下列工作:

  • 控制硬件与软件
  • 监控事件
  • 就某个事件运行一个脚本
  • 就某个事件发出一封Email

MSDN 中关于WMI的内容:Windows Management Instrumentation



Paul Li 是在纽约的 Dell 专业服务的首席软件安全顾问

用户不喜欢反应慢的程序。在执行耗时较长的操作时,使用多线程是明智之举,它可以提高程序 UI 的响应速度,使得一切运行显得更为快速。在 Windows 中进行多线程编程曾经是 C++ 开发人员的专属特权,但是现在,可以使用所有兼容 Microsoft .NET 的语言来编写。

不过Windows 窗体体系结构对线程使用制定了严格的规则。如果只是编写单线程应用程序,则没必要知道这些规则,这是因为单线程的代码不可能违反这些规则。然而,一旦采用多线程,就需要理解 Windows 窗体中最重要的一条线程规则:除了极少数的例外情况,否则都不要在它的创建线程以外的线程中使用控件的任何成员。本规则的例外情况有文档说明,但这样的情况非常少。这适用于其类派生自 System.Windows.Forms.Control 的任何对象,其中几乎包括 UI 中的所有元素。所有的 UI 元素(包括表单本身)都是从 Control 类派生的对象。此外,这条规则的结果是一个被包含的控件(如,包含在一个表单中的按钮)必须与包含它控件位处于同一个线程中。也就是说,一个窗口中的所有控件属于同一个 UI 线程。实际中,大部分 Windows 窗体应用程序最终都只有一个线程,所有 UI 活动都发生在这个线程上。这个线程通常称为 UI 线程。这意味着您不能调用用户界面中任意控件上的任何方法,除非在该方法的文档说明中指出可以调用。该规则的例外情况(总有文档记录)非常少而且它们之间关系也不大。请注意,以下代码是非法的:

        private Thread myThread;

        private void Form1_Load(object sender, EventArgs e)

        {

            myThread = new Thread(new ThreadStart(RunsOnWorkerThread));

            myThread.Start();

        }

        private void RunsOnWorkerThread()

        {

            label1.Text = "myThread线程调用UI控件";

    }

如果您在 .NET Framework 1.0 版本中尝试运行这段代码,也许会侥幸运行成功,或者初看起来是如此。这就是多线程错误中的主要问题,即它们并不会立即显现出来。甚至当出现了一些错误时,在第一次演示程序之前一切看起来也都很正常。但不要搞错我刚才显示的这段代码明显违反了规则,并且可以预见,任何抱希望于“试运行时良好,应该就没有问题”的人在即将到来的调试期是会付出沉重代价的。

 

下面我们来看看有哪些方法可以解决这一问题。

 

 

一、System.Windows.Forms.MethodInvoker 类型是一个系统定义的委托,用于调用不带参数的方法。

 

        private Thread myThread;

        private void Form1_Load(object sender, EventArgs e)

        {

            myThread = new Thread(new ThreadStart(RunsOnWorkerThread));

            myThread.Start();

        }

        private void RunsOnWorkerThread()

        {

            MethodInvoker mi = new MethodInvoker(SetControlsProp);

            BeginInvoke(mi);

        }

        private void SetControlsProp()

        {

            label1.Text = "myThread线程调用UI控件";

        }

二、直接用System.EventHandle(可带参数)

        private Thread myThread;

        private void Form1_Load(object sender, EventArgs e)

        {

            myThread = new Thread(new ThreadStart(RunsOnWorkerThread));

            myThread.Start();

        }

        private void RunsOnWorkerThread()

        {

            //DoSomethingSlow();

            string pList = "myThread线程调用UI控件";

            label1.BeginInvoke(new System.EventHandler(UpdateUI), pList);

        }

        //直接用System.EventHandler,没有必要自定义委托

        private void UpdateUI(object o, System.EventArgs e)

        {

           //UI线程设置label1属性

            label1.Text = o.ToString() + "成功!";

        }

 

 

三、包装 Control.Invoke

 

虽然第二个方法中的代码解决了这个问题,但它相当繁琐。如果辅助线程希望在结束时提供更多的反馈信息,而不是简单地给出“Finished!”消息,则 BeginInvoke 过于复杂的使用方法会令人生畏。为了传达其他消息,例如“正在处理”、“一切顺利”等等,需要设法向 UpdateUI 函数传递一个参数。可能还需要添加一个进度栏以提高反馈能力。这么多次调用 BeginInvoke 可能导致辅助线程受该代码支配。这样不仅会造成不便,而且考虑到辅助线程与 UI 的协调性,这样设计也不好。对这些进行分析之后,我们认为包装函数可以解决这两个问题。

        private Thread myThread;

        private void Form1_Load(object sender, EventArgs e)

        {

            myThread = new Thread(new ThreadStart(RunsOnWorkerThread));

            myThread.Start();

        }

        private void RunsOnWorkerThread()

        {

            ////DoSomethingSlow();

            for (int i = 0; i < 100; i++)

            {

                ShowProgress( Convert.ToString(i)+"%", i);

                Thread.Sleep(100);

            }

        }

        public void ShowProgress(string msg, int percentDone)

        {

            // Wrap the parameters in some EventArgs-derived custom class:

            System.EventArgs e = new MyProgressEvents(msg, percentDone);

            object[] pList = { this, e };

 

            BeginInvoke(new MyProgressEventsHandler(UpdateUI), pList);

        }

        private delegate void MyProgressEventsHandler(object sender, MyProgressEvents e);

        private void UpdateUI(object sender, MyProgressEvents e)

        {

            lblStatus.Text = e.Msg;

            myProgressControl.Value = e.PercentDone;

        }

    public class MyProgressEvents : EventArgs

    {

        public string Msg;

        public int PercentDone;

        public MyProgressEvents(string msg, int per)

        {

            Msg = msg;

            PercentDone = per;

        }

 }

ShowProgress 方法对将调用引向正确线程的工作进行封装。这意味着辅助线程代码不再担心需要过多关注 UI 细节,而只要定期调用 ShowProgress 即可。

如果我提供一个设计为可从任何线程调用的公共方法,则完全有可能某人会从 UI 线程调用这个方法。在这种情况下,没必要调用 BeginInvoke,因为我已经处于正确的线程中。调用 Invoke 完全是浪费时间和资源,不如直接调用适当的方法。为了避免这种情况,Control 类将公开一个称为 InvokeRequired 的属性。这是“只限 UI 线程”规则的另一个例外。它可从任何线程读取,如果调用线程是 UI 线程,则返回假,其他线程则返回真。这意味着我可以按以下方式修改包装:

        public void ShowProgress(string msg, int percentDone)

        {

            if (InvokeRequired)

            {

                // As before

                //…

            }

            else

            {

                // We’re already on the UI thread just

                // call straight through.

                UpdateUI(this, new MyProgressEvents(msg,PercentDone));

            }

        }

参考:

http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/misMultithreading.mspx?mfr=true

 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=715064
2007年02月01日
  • 1.mysqlimport的语法介绍:

mysqlimport位于mysql/bin目录中,是mysql的一个载入(或者说导入)数据的一个非常有效的工具。这是一个命令行工具。有两个参数以及大量的选项可供选择。这个工具把一个文本文件(text file)导入到你指定的数据库和表中。比方说我们要从文件Customers.txt中把数据导入到数据库Meet_A_Geek中的表Custermers中:

mysqlimport Meet_A_Geek Customers.txt

注意:这里Customers.txt是我们要导入数据的文本文件, 而Meet_A_Geek是我们要操作的数据库, 数据库中的表名是Customers,这里文本文件的数据格式必须与Customers表中的记录格式一致,否则mysqlimport命令将会出错。

其中表的名字是导入文件的第一个句号(.)前面文件字符串,另外一个例子:

mysqlimport Meet_A_Geek Cus.to.mers.txt

那么我们将把文件中的内容导入到数据库Meet_A_Geek 中的Cus表中。

上面的例子中,都只用到两个参数,并没有用到更多的选项,下面介绍mysqlimport的选项

2.mysqlimport的常用选项介绍:

选项 功能

-d or –delete 新数据导入数据表中之前删除数据数据表中的所有信息

-f or –force 不管是否遇到错误,mysqlimport将强制继续插入数据

-i or –ignore mysqlimport跳过或者忽略那些有相同唯一

关键字的行, 导入文件中的数据将被忽略。

-l or -lock-tables 数据被插入之前锁住表,这样就防止了, 你在更新数据库时,用户的查询和更新受到影响。

-r or -replace 这个选项与-i选项的作用相反;此选项将替代 表中有相同唯一关键字的记录。

–fields-enclosed- by= char 指定文本文件中数据的记录时以什么括起的, 很多情况下 数据以双引号括起。 默认的情况下数据是没有被字符括起的。

–fields-terminated- by=char 指定各个数据的值之间的分隔符,在句号分隔的文件中, 分隔符是句号。您可以用此选项指定数据之间的分隔符。 默认的分隔符是跳格符(Tab)

–lines-terminated- by=str 此选项指定文本文件中行与行之间数据的分隔字符串 或者字符。 默认的情况下mysqlimport以newline为行分隔符。 您可以选择用一个字符串来替代一个单个的字符: 一个新行或者一个回车。

mysqlimport命令常用的选项还有-v 显示版本(version), -p 提示输入密码(password)等。

3.例子:导入一个以逗号为分隔符的文件

文件中行的记录格式是这样的:

"1", "ORD89876", "1 Dozen Roses", "19991226"

我们的任务是要把这个文件里面的数据导入到数据库Meet_A_Geek中的表格Orders中, 我们使用这个命令:

bin/mysqlimport –prl –fields-enclosed-by=" –fields-terminated-by=, Meet_A_Geek Orders.txt

这个命令可能看起来很不爽,不过当你熟悉了之后,这是非常简单的。第一部分,bin/mysqlimport,告诉操作系统你要运行的命令是mysql/bin目录下的mysqlimport,选项p是要求输入密码,这样就要求你在改动数据库之前输入密码,操作起来会更安全。 我们用了r选项是因为我们想要把表中的唯一关键字与文件记录中有重复唯一关键字的记录替换成文件中的数据。我们表单中的数据不是最新的,需要用文件中的数据去更新,因而就用r这个选项,替代数据库中已经有的记录。l选项的作用是在我们插入数据的时候锁住表,这样就阻止了用户在我们更新表的时候对表进行查询或者更改的操作。

批处理导入文件,从sql文件导入数据到数据库中 ,批处理是一种非交互式运行mysql程序的方法,如同您在mysql中使用的命令一样,你仍然将使用这些命令。

为了实现批处理,您重定向一个文件到mysql程序中,首先我们需要一个文本文件,这个文本文件包含有与我们在mysql中输入的命令相同的文本。 //www.w3sky.com

比如我们要插入一些数据,使用包含下面文本的文件(文件名为New_Data.sql,当然我们也可以取名为New_Data.txt及任何其他的合法名字,并不一定要以后缀sql结尾):

USE Meet_A_Geek;

INSERT INTO Customers (Customer_ID, Last_Name) VALUES(NULL, "Block");

INSERT INTO Customers (Customer_ID, Last_Name) VALUES(NULL, "Newton");

INSERT INTO Customers (Customer_ID, Last_Name) VALUES(NULL, "Simmons");

注意上面的这些句子的语法都必须是正确的,并且每个句子以分号结束。 上面的USE命令选择数据库,INSERT命令插入数据。

下面我们要把上面的文件导入到数据库中,导入之前要确认数据库已经在运行,即是mysqld进程(或者说服务,Windows NT下面称为”服务“,unix下面为”进程“)已经在运行。

 

然后运行下面的命令:

bin/mysql –p < /home/mark/New_Data.sql

接着按提示输入密码,如果上面的文件中的语句没有错误,那么这些数据就被导入到了数据库中。

命令行中使用LOAD DATA INFILE 从文件中导入数据到数据库:

现在您可能会问自己,"究竟为什么我要输入所有的这些SQL语句到文件中,然后通过程序运行它们呢?” 这样看起来好像需要大量的工作。很好,你这样想很可能就对了。但是假如你有从所有这些命令中产生的log记录呢?现在这样就很棒,嗯,大多数数据库都会自动产生数据库中的事件记录的log。而大部分log都包含有用过的原始的SQL命令。因此,如果您不能从您现在的数据库中导出数据到新的mysql数据库中使用,那么您可以使用log和mysql的批处理特性,来快速且方便地导入您地数据。当然,这样就省去了打字的麻烦。

LOAD DATA INFILE

这是我们要介绍的最后一个导入数据到MySQL数据库中的方法。这个命令与mysqlimport非常相似,但这个方法可以在mysql命令行中使用。也就是说您可以在所有使用API的程序中使用这个命令。使用这种方法,您就可以在应用程序中导入您想要导入的数据。 使用这个命令之前,mysqld进程(服务)必须已经在运行。

启动mysql命令行:

bin/mysql –p

按提示输入密码,成功进入mysql命令行之后,输入下面的命令:

USE Meet_A_Geek;

LOAD DATA INFILE "/home/mark/data.sql" INTO TABLE Orders;

简单的讲,这样将会把文件data.sql中的内容导入到表Orders中,如mysqlimport工具一样,这个命令也有一些可以选择的参数。比如您需要把自己的电脑上的数据导入到远程的数据库服务器中,您可以使用下面的命令:

LOAD DATA LOCAL INFILE "C:\MyDocs\SQL.txt" INTO TABLE Orders;

上面的LOCAL参数表示文件是本地的文件,服务器是您所登陆的服务器。 这样就省去了使用ftp来上传文件到服务器,MySQL替你完成了. 您也可以设置插入语句的优先级,如果您要把它标记为低优先级(LOW_PRIORITY),那么MySQL将会等到没有其他人读这个表的时候,才把插入数据。可以使用如下的命令:

LOAD DATA LOW_PRIORITY INFILE "/home/mark/data.sql" INTO TABLE Orders;

您也可以指定是否在插入数据的时候,取代或者忽略文件与数据表中重复的键值。替代重复的键值的语法:

LOAD DATA LOW_PRIORITY INFILE "/home/mark/data.sql" REPLACE INTO TABLE Orders;

上面的句子看起来有点笨拙,但却把关键字放在了让您的剖析器可以理解的地方。 //from www.w3sky.com

下面的一对选项描述了文件的记录格式,这些选项也是在mysqlimport工具中可以用的。他们在这里看起来有点不同。首先,要用到FIELDS关键字,如果用到这个关键字,MySQL剖析器希望看到至少有下面的一个选项:

TERMINATED BY character

ENCLOSED BY character

ESCAPED BY character

这些关键字与它们的参数跟mysqlimport中的用法是一样的. The TERMINATED BY 描述字段的分隔符,默认情况下是tab字符(\t)

ENCLOSED BY描述的是字段的括起字符。比方以引号括起每一个字段。

ESCAPED BY 描述的转义字符。默认的是反些杠(backslash:\ ).

下面仍然使用前面的mysqlimport命令的例子,用LOAD DATA INFILE语句把同样的文件导入到数据库中:

LOAD DATA INFILE "/home/mark/Orders.txt" REPLACE INTO TABLE Orders FIELDS TERMINATED BY ‘,’ ENCLOSED BY ‘"’;

LOAD DATA INFILE语句中有一个mysqlimport工具中没有特点:

LOAD DATA INFILE 可以按指定的列把文件导入到数据库中。 当我们要把数据的一部分内容导入的时候,这个特点就很重要。比方说,我们要从Access数据库升级到MySQL数据库的时候,需要加入一些栏目(列/字段/field)到MySQL数据库中,以适应一些额外的需要。

这个时候,我们的Access数据库中的数据仍然是可用的,但是因为这些数据的栏目(field)与MySQL中的不再匹配,因此而无法再使用mysqlimport工具。尽管如此,我们仍然可以使用LOAD DATA INFILE,下面的例子显示了如何向指定的栏目(field)中导入数据:

LOAD DATA INFILE "/home/Order.txt" INTO TABLE Orders(Order_Number, Order_Date, Customer_ID);

如您所见,我们可以指定需要的栏目(fields)。这些指定的字段依然是以括号括起,由逗号分隔的,如果您遗漏了其中任何一个,MySQL将会提醒您

导出数据的方法:Methods of Exporting Data

您可以看到MySQL有很多可以导入数据的方法,然而这些只是数据传输中的一半。另外的一般是从MySQL数据库中导出数据。有许多的原因我们需要导出数据。一个重要的原因是用于备份数据库。数据的造价常常是昂贵的,需要谨慎处理它们。经常地备份可以帮助防止宝贵数据地丢失;另外一个原因是,也许您希望导出数据来共享。 在这个信息技术不断成长的世界中,共享数据变得越来越常见。

比方说Macmillan USA维护护着一个将要出版的书籍的大型数据库。这个数据库在许多书店之间共享,这样他们就知道哪些书将会很快出版。医院越来越走向采用无纸病历记录,这样这些病历可以随时跟着你。世界变得越来越小,信息也被共享得越来越多。有很多中导出数据得方法,它们都跟导入数据很相似。因为,毕竟,这些都只是一种透视得方式。从数据库导出的数据就是从另一端导入的数据。这里我们并不讨论其他的数据库各种各样的导出数据的方法,您将学会如何用MySQL来实现数据导出。

使用mysqldump:

 

(mysqldump命令位于mysql/bin/目录中)

mysqldump工具很多方面类似相反作用的工具mysqlimport。它们有一些同样的选项。但mysqldump能够做更多的事情。它可以把整个数据库装载到一个单独的文本文件中。这个文件包含有所有重建您的数据库所需要的SQL命令。这个命令取得所有的模式(Schema,后面有解释)并且将其转换成DDL语法(CREATE语句,即数据库定义语句),取得所有的数据,并且从这些数据中创建INSERT语句。这个工具将您的数据库中所有的设计倒转。因为所有的东西都被包含到了一个文本文件中。这个文本文件可以用一个简单的批处理和一个合适SQL语句导回到MySQL中。这个工具令人难以置信地简单而快速。决不会有半点让人头疼地地方。

因此,如果您像装载整个数据库Meet_A_Geek的内容到一个文件中,可以使用下面的命令:

bin/mysqldump –p Meet_A_Geek > MeetAGeek_Dump_File.txt

这个语句也允许您指定一个表进行dump(备份/导出/装载?)。如果您只是希望把数据库Meet_A_Geek中的表Orders中的整个内容导出到一个文件,可以使用下面的命令:

bin/mysqldump –p Meet_A_Geek Orders >MeetAGeek_Orders.txt

这个非常的灵活,您甚至可以使用WHERE从句来选择您需要的记录导出到文件中。要达到这样的目的,可以使用类似于下面的命令:

bin/mysqldump –p –where="Order_ID > 2000" Meet_A_Geek Orders > Special_Dump.txt

mysqldump工具有大量的选项,部分选项如下:

选项/Option 作用/Action Performed

–add-drop-table

这个选项将会在每一个表的前面加上DROP TABLE IF

EXISTS语句,这样可以保证导回MySQL数据库的时候不会出错,因为每次导回的时候,都会首先检查表是否存在,存在就删除

–add-locks

这个选项会在INSERT语句中捆上一个LOCK TABLE和UNLOCK

TABLE语句。这就防止在这些记录被再次导入数据库时其他用户对表进行的操作

-c or – complete_insert

这个选项使得mysqldump命令给每一个产生INSERT语句加上列(field)的名字。当把数据导出导另外一个数据库时这个选项很有用。

–delayed-insert 在INSERT命令中加入DELAY选项

-F or -flush-logs 使用这个选项,在执行导出之前将会刷新MySQL服务器的log.

-f or -force 使用这个选项,即使有错误发生,仍然继续导出

–full 这个选项把附加信息也加到CREATE TABLE的语句中

-l or -lock-tables 使用这个选项,导出表的时候服务器将会给表加锁。

-t or -no-create- info

这个选项使的mysqldump命令不创建CREATE TABLE语句,这个选项在您只需要数据而不需要DDL(数据库定义语句)时很方便。

-d or -no-data 这个选项使的mysqldump命令不创建INSERT语句。

在您只需要DDL语句时,可以使用这个选项。

–opt 此选项将打开所有会提高文件导出速度和创造一个可以更快导入的文件的选项。 //from www.w3sky.com

-q or -quick 这个选项使得MySQL不会把整个导出的内容读入内存再执行导出,而是在读到的时候就写入导文件中。

-T path or -tab = path 这个选项将会创建两个文件,一个文件包含DDL语句或者表创建语句,另一个文件包含数据。DDL文件被命名为table_name.sql,数据文件被命名为table_name.txt.路径名是存放这两个文件的目录。目录必须已经存在,并且命令的使用者有对文件的特权。

-w "WHERE Clause" or -where = "Where clause "

如前面所讲的,您可以使用这一选项来过筛选将要放到 导出文件的数据。

假定您需要为一个表单中要用到的帐号建立一个文件,经理要看今年(2004年)所有的订单(Orders),它们并不对DDL感兴趣,并且需要文件有逗号分隔,因为这样就很容易导入到Excel中。 为了完成这个人物,您可以使用下面的句子:

bin/mysqldump –p –where "Order_Date >=’2000-01-01′"

–tab = /home/mark –no-create-info –fields-terminated-by=, Meet_A_Geek Orders

这将会得到您想要的结果。

schema:模式

The set of statements, expressed in data definition language, that completely describe the structure of a data base. 一组以数据定义语言来表达的语句集,该语句集完整地描述了数据库的结构。

SELECT INTO OUTFILE :

如果您觉得mysqldump工具不够酷,就使用SELECT INTO OUTFILE吧, MySQL同样提供一个跟LOAD DATA INFILE命令有相反作用的命令,这就是SELECT INTO OUTFILE 命令,这两个命令有很多的相似之处。首先,它们有所有的选项几乎相同。现在您需要完成前面用mysqldump完成的功能,可以依照下面的步骤进行操作:

1. 确保mysqld进程(服务)已经在运行

2. cd /usr/local/mysql

3. bin/mysqladmin ping ;// 如果这个句子通不过,可以用这个:mysqladmin -u root -p ping mysqladmin ping用于检测mysqld的状态,is alive说明正在运行,出错则可能需要用户名和密码。

4. 启动MySQL 监听程序.

5. bin/mysql –p Meet_A_Geek;// 进入mysql命令行,并且打开数据库Meet_A_Geek,需要输入密码

6. 在命令行中,输入一下命令:

SELECT * INTO OUTFILE ‘/home/mark/Orders.txt’

FIELDS

TERMINATED BY = ‘,’

FROM Orders

WHERE Order_Date >= ‘2000-01-01′

在你按了Return(回车)之后,文件就创建了。这个句子就像一个规则的SELECT语句,只是把想屏幕的输出重定向到了文件中。这意味这您可以使用JOIN来实现多表的高级查询。这个特点也可以被用作一个报表产生器。

比方说,您可以组合这一章中讨论的方法来产生一个非常有趣的查询,试试这个:

在mysql目录建立一个名为Report_G.rpt 的文本文件,加入下面的行:

USE Meet_A_Geek;

INSERT INTO Customers (Customer_ID, Last_Name, First_Name)

VALUES (NULL, "Kinnard", "Vicky");

INSERT INTO Customers (Customer_ID, Last_Name, First_Name)

VALUES (NULL, "Kinnard", "Steven");

INSERT INTO Customers (Customer_ID, Last_Name, First_Name)

VALUES (NULL, "Brown", "Sam");

SELECT Last_Name INTO OUTFILE ‘/home/mark/Report.rpt’

FROM Customers WHERE Customer_ID > 1; 

然后确认 mysql进程在运行,并且您在mysql目录中, 输入下面的命令:

bin/mysql < Report_G.rpt检查您命名作为输出的文件,这个文件将会包含所有您在Customers表中输入的顾客的姓。 如您所见,您可以使用今天学到的导入/导出(import/export)的方法来帮助得到报表。

翻译声明: 本文内容来自Sam’s Teach Yourself MySQL in 21 Days一书的部分内容,by Mark Maslakowski 英文原文版权属原作者所有,中文的部分翻译有略有增删;原书讲的过于清楚的地方有删,讲的不清楚的地方有增;如果有翻译的不妥或者不正确的地方,请指正。

2007年01月29日

MySQL使用精华(上九天揽月)  
  声明:欢迎引用转载,但不得删减任何内容,包括作者和声明。  
  作者介绍:上九天揽月   一著名信息安全公司项目经理,精通Java开发架构和项目的管理。联系方式:jawing@xinhuanet.com  
   
  1.   临时表的使用  
  MySQL对于通过OR进行的两个或两个以上关键字的查询还没有优化(但是针对与一个关键字的不同的OR连接条件的查询则做得很好):  
  SELECT   field1_index,   field2_index   FROM   test_table   WHERE   field1_index   =   ‘1′   OR     field2_index   =   ‘1′  
  其主要原因是MySQL开发组还没有足够的时间拿出解决这类问题的有效的方案。(相对来说,AND条件查询则做得非常的好)  
  但是我们可以通过临时表的使用非常好的解决这个问题,同时,当我们进行复杂查询的时候亦可以使用这种方法进行性能的优化。  
  CREATE   TEMPORARY   TABLE   tmp   SELECT   field1_index,   field2_index   FROM   test_table   WHERE   field1_index   =   ‘1′;  
  INSERT   INTO   tmp   SELECT   field1_index,   field2_index   FROM   test_table   WHERE   field2_index   =   ‘1′;  
  SELECT   *   from   tmp;  
  DROP   TABLE   tmp;  
  上面这种方法解决了联合查询的效率问题。  
  2.   用户变量的使用  
  我们可以使用MySQL的用户变量来记住不需要保存到客户端的临时变量里的结果,比如,如果我们要查找最高和最低价格的书籍可以使用如下的方法:  
  select   @min_price:=min(price),@max_price:=max(price)   from   shop;  
  select   *   from   shop   where   price=@min_price   or   price=@max_price;  
   
  +———+——–+——-+  
  |   article   |   dealer   |   price   |  
  +———+——–+——-+  
  |         0003   |   D             |     1.25   |  
  |         0004   |   D             |   19.95   |  
  +———+——–+——-+  
  3.   查找每类东西的最高或最低的方法  
  比如查找每个作者所发表书籍中最贵的书的价格,可以使用如下方法:  
  SELECT   article,   MAX(price)   AS   price  
  FROM       shop  
  GROUP   BY   article  
   
  +———+——-+  
  |   article   |   price   |  
  +———+——-+  
  |         0001   |     3.99   |  
  |         0002   |   10.99   |  
  |         0003   |     1.69   |  
  |         0004   |   19.95   |  
  +———+——-+  
  4.   获得数据库和表单的信息的方法  
  获得数据库的方法有:show   databases;   这将列出所有的存在的数据库。  
   
  获得现在正在使用的数据库的方法:select   database();   这将列出目前正在使用的数据库。如果你目前没有选择任何数据库,则结果是空。  
   
  获得目前数据库中包含的表单的方法:show   tables;   这将列出目前正在使用的数据库中的所有的表单的名称。  
   
  获得表单结构信息的方法:describe   table_name;   这将列出所选表单的结构信息。  
   
  获得一个表单的索引的新方法:show   index   from   table_name;   这将显示所选表单的索引信息。  
  5.   时间日期的计算  
  MySQL提供了几个用于日期计算的函数,例如,计算年龄等。  
  为了得到宠物的年龄,就必须计算宠物生日日期与当前日期在年份上的差,如果当前日期的月份小于宠物生日的月份,则还需减去1。下面的查询就可以获得每只宠物的年龄大小:  
  mysql>   SELECT   name,   birth,   CURRENT_DATE,  
          ->   (YEAR(CURRENT_DATE)-YEAR(birth))  
          ->   –   (RIGHT(CURRENT_DATE,5)<RIGHT(birth,5))  
          ->   AS   age  
          ->   FROM   pet;  
  +———-+————+————–+——+  
  |   name           |   birth             |   CURRENT_DATE   |   age     |  
  +———-+————+————–+——+  
  |   Fluffy       |   1993-02-04   |   2001-08-29       |         8   |  
  |   Claws         |   1994-03-17   |   2001-08-29       |         7   |  
  |   Buffy         |   1989-05-13   |   2001-08-29       |       12   |  
  |   Fang           |   1990-08-27   |   2001-08-29       |       11   |  
  |   Bowser       |   1989-08-31   |   2001-08-29       |       11   |  
  |   Chirpy       |   1998-09-11   |   2001-08-29       |         2   |  
  |   Whistler   |   1997-12-09   |   2001-08-29       |         3   |  
  |   Slim           |   1996-04-29   |   2001-08-29       |         5   |  
  |   Puffball   |   1999-03-30   |   2001-08-29       |         2   |  
  +———-+————+————–+——+  
  在这里,year()函数可以获得一个日期的年份部分。right()函数就可以获得一个日期从右边开始数的所定个数的字符,如right(“2001-08-29”,5)   就可以获得08-29。current_date是个系统参数,代表当前系统日期。  
  另外month(),dayofmonth()也是非常有用的函数,它们可以获得日期的月份以及当前日期是这个月中的第几天。例如:    
  mysql>   SELECT   name,   birth,   MONTH(birth)   FROM   pet;  
  +———-+————+————–+  
  |   name           |   birth             |   MONTH(birth)   |  
  +———-+————+————–+  
  |   Fluffy       |   1993-02-04   |                         2   |  
  |   Claws         |   1994-03-17   |                         3   |  
  |   Buffy         |   1989-05-13   |                         5   |  
  |   Fang           |   1990-08-27   |                         8   |  
  |   Bowser       |   1989-08-31   |                         8   |  
  |   Chirpy       |   1998-09-11   |                         9   |  
  |   Whistler   |   1997-12-09   |                       12   |  
  |   Slim           |   1996-04-29   |                         4   |  
  |   Puffball   |   1999-03-30   |                         3   |  
  +———-+————+————–+  
  或者:查询5月份出生的宠物  
  mysql>   SELECT   name,   birth   FROM   pet   WHERE   MONTH(birth)   =   5;  
  +——-+————+  
  |   name     |   birth             |  
  +——-+————+  
  |   Buffy   |   1989-05-13   |  
  +——-+————+  
  以上两种查询方式在实际应用中也是经常用到的。  
  另外now()函数可以获得当前的日期时间值,date_add()函数可以给某个日期时间值加上特定的时间。例如我要查询当前时间后一个月份的出生的宠物。    
  mysql>   SELECT   name,   birth   FROM   pet  
          ->   WHERE   MONTH(birth)   =   MONTH(DATE_ADD(NOW(),   INTERVAL   1   MONTH));  
  另外一种方法也可以实现上面的要求,就是使用取模函数mod()。    
  mysql>   SELECT   name,   birth   FROM   pet  
          ->   WHERE   MONTH(birth)   =   MOD(MONTH(NOW()),   12)   +   1;  
  使用mod()函数是为了解决12月份到1月份的问题。  
   
  在第二部分中将详细的讲解MySQL中包含的在实际开发中经常使用的函数的用法。

2007年01月24日
一个简单的MP3播放器
2006-07-13 21:39
利用API函数[mciSendString]可以轻松实现MP3音乐文件的播放。下面这段程序实现了MP3播放的大部分常规操作,对其稍加修改,做一个100KB大小的MP3播放器轻而易举

启动VB程序,在窗体上放置6个命令按钮,三个标签,一个公用对话框、一个进度条、一个状态栏和一个计时器,窗本的布置请参考附图

按钮"Open MP3 File"是用来打开对话框选择MP3文件,其他5个按钮分别为Play(播放)、Pause(暂停)、Stopplay(停止播放)、Back(向后跳跃)、Prew(向前跳跃)。Label1用来表示歌曲当前时间;label2放在时度条的最左边,Caption属性为"00:00";lable3放在时度条的右边,用来表示歌曲总长。

下面就可以编写代码了。首先在窗体的"通用声明"部分声明函数
Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA"(Byval lpstrCommand As String,ByVal lpstrRetumString As String,ByVal uReturnLength As long,ByVal hwndCallback as long) As long

Dim mfn As String

下面就是各个对象的代码了:
Private Sub Form_load()
mfn=""
play.Enabled=False
pause.Enabled=False
Stopplay.Enabled=False
back.Enabled=False
prew.Enabled=False
Timer1.Enabled=False
Timer1.Interval=500
End Sub

Private Sub open_Click()
On Error Goto err
With CommonDialog1
.CancelError=True
.Filter="音乐文件|*.mp3;*.wav;*.mid"
.Flags=cdlOFNNoChangeDir And cdlOFNPathMustExist
.Action=1
stopplay_Click ‘停止按钮同时可以设备初始化
mfn=.FileName ‘这个程序中文件名一定不能带空格
Form1.Caption=.FileName
play.Enabled=True
End With
play_Click
Exit Sub
err:
End Sub

Private Sub play_Click()
On Error Resume Next
Dim t As Long
t=mciSendString("open " + mfn,0&,0,0) ‘Open后边的空格一定不能丢
Dim ret As String * 128
t=mciSendString("status " + mfn + " length",ret,128,0)
‘显示歌曲总长
ret=Left(ret,8)
If ret<>"" Then
ProgressBar1.Min=0
ProgressBar1.Max=Val(ret)
Label3.Caption=gettime(Val(ret))
End If
play.Enabled=False
pause.Enabled=True
stopplay.Enabled=True
back.Enabled=True
prew.Enabled=True
t=mciSendString("status " + mfn +" mode",ret,128,0)
‘得到设备的当前状态,是播放还是暂停等等
ret=Left(ret,8)
StatusBar1.Panels(1).text=ret
‘在状态栏显示播放状态
t=mciSendString("play " + mfn + " form " + Str(ProgressBar1.Value),0&,0,0)
‘开始播放
Timer1.Enabled=True
End Sub

Private Sub pause_Click()
t%=mciSendString("pause " + mfn,0&,0,0)
‘发出暂停的命令
play.Enable=True
pause.Enabled=False
stopplay.Enabled=True
back.Enabled=False
prew.Enabled=False
End Sub

Private Sub stopplay_Click()
t%=mciSendString("stop " + mfn,0&,0,0)
t%=mciSendString("close " + mfn,0&,0,0)
‘停止播放
play.Enabled=True
pause.Enabled=False
stopplay.Enabled=False
back.Enabled=False
prew.Enabled=False
End Sub

Private Sub back_Click()
t%=mciSendString("play " + mfn + " from " + Str(ProgressBar1.Value-(ProgressBar1.Max\10)),0&,0,0) ‘向后跳一小段再播放
End Sub

Private Sub prew_Click()
t%=mciSendString("play " + mfn + " from " + Stri(ProgressBar1.Value+(ProgressBar1.Max\10)),0&,0,0) ‘向前跳一小段再播放
End Sub

Private Sub Timer1_Timer()
Dim t As Long
Dim ret As String * 128
t=mciSendString("status " + mfn + " position",ret,0,0)
‘得到当前播放位置
ret=left(ret,8)
ProgressBar1.Value=Val(ret)
Label1.Caption=gettime(Val(ret))
‘显示歌曲当前时间
If ProgressBar1.Value=ProgressBar1.Max Then
stopplay_Click
End If
t=mciSendString("status " + mfn + " mode",ret,128,0)
ret=Left(ret,8)
StatusBar1.Panels(1).Text=ret
End Sub

Private Sub Form_Unload(Cacel As Integer)
t%=mciSendString("stop " + mfn,0&,0,0)
t%=mciSendString("close " + mfn,0&,0,0)
End Sub

Private Function Gettime(position As Long) As String
‘这个函数的功能是把以长整型表示的时间转换为电子钟式的"**:**"
Dim min,sec
min=position/1000
min=min/60
sec=min-Int(min)
min=Int(min)
sec=60 * sec / 100
sec=Int(sec * 100)
gettime=Str(min) + ":" + Str(sec)
End Function

图中5个控制按钮,其实是把字体设为"Webdings",让它们的Caption属性分别为"4" ";" "<" "7" "8"
就可以了

2007年01月15日

摘要

如果应用程序在控制用户界面的线程上执行非 UI 处理,则会使应用程序的运行显得缓慢而迟钝,让用户难以忍受。但是长期以来,编写适用于 Windows 的多线程应用程序只限于 C++ 开发人员。现在有了 .NET Framework,您就可以充分利用 C# 中的多线程来控制程序中的指令流,并使 UI 线程独立出来以便用户界面能够迅速响应。本文将向您介绍如何实现这一目标。此外,本文还将讨论多线程的缺陷并提供一个框架来保护并发线程执行的安全。

*

本页内容
为什么选择多线程? 为什么选择多线程?
异步委托调用 异步委托调用
线程和控件 线程和控件
在正确的线程中调用控件 在正确的线程中调用控件
包装 Control.Invoke 包装 Control.Invoke
锁定 锁定
死锁 死锁
使其简单 使其简单
取消 取消
程序关闭 程序关闭
错误处理 错误处理
小结 小结

用户不喜欢反应慢的程序。程序反应越慢,就越没有用户会喜欢它。在执行耗时较长的操作时,使用多线程是明智之举,它可以提高程序 UI 的响应速度,使得一切运行显得更为快速。在 Windows 中进行多线程编程曾经是 C++ 开发人员的专属特权,但是现在,可以使用所有兼容 Microsoft .NET 的语言来编写,其中包括 Visual Basic.NET。不过,Windows 窗体对线程的使用强加了一些重要限制。本文将对这些限制进行阐释,并说明如何利用它们来提供快速、高质量的 UI 体验,即使是程序要执行的任务本身速度就较慢。

为什么选择多线程?

多线程程序要比单线程程序更难于编写,并且不加选择地使用线程也是导致难以找到细小错误的重要原因。这就自然会引出两个问题:为什么不坚持编写单线程代码?如果必须使用多线程,如何才能避免缺陷呢?本文的大部分篇幅都是在回答第二个问题,但首先我要来解释一下为什么确实需要多线程。

多线程处理可以使您能够通过确保程序“永不睡眠”从而保持 UI 的快速响应。大部分程序都有不响应用户的时候:它们正忙于为您执行某些操作以便响应进一步的请求。也许最广为人知的例子就是出现在“打开文件”对话框顶部的组合框。如果在展开该组合框时,CD-ROM驱动器里恰好有一张光盘,则计算机通常会在显示列表之前先读取光盘。这可能需要几秒钟的时间,在此期间,程序既不响应任何输入,也不允许取消该操作,尤其是在确实并不打算使用光驱的时候,这种情况会让人无法忍受。

执行这种操作期间 UI 冻结的原因在于,UI 是个单线程程序,单线程不可能在等待 CD-ROM驱动器读取操作的同时处理用户输入,如图 1 所示。“打开文件”对话框会调用某些阻塞 (blocking) API 来确定 CD-ROM 的标题。阻塞 API 在未完成自己的工作之前不会返回,因此这期间它会阻止线程做其他事情。

图 1 单线程

在多线程下,像这样耗时较长的任务就可以在其自己的线程中运行,这些线程通常称为辅助线程。因为只有辅助线程受到阻止,所以阻塞操作不再导致用户界面冻结,如图 2 所示。应用程序的主线程可以继续处理用户的鼠标和键盘输入的同时,受阻的另一个线程将等待 CD-ROM 读取,或执行辅助线程可能做的任何操作。

图 2 多线程

其基本原则是,负责响应用户输入和保持用户界面为最新的线程(通常称为 UI 线程)不应该用于执行任何耗时较长的操作。惯常做法是,任何耗时超过 30ms 的操作都要考虑从 UI 线程中移除。这似乎有些夸张,因为 30ms 对于大多数人而言只不过是他们可以感觉到的最短的瞬间停顿,实际上该停顿略短于电影屏幕中显示的连续帧之间的间隔。

如果鼠标单击和相应的 UI 提示(例如,重新绘制按钮)之间的延迟超过 30ms,那么操作与显示之间就会稍显不连贯,并因此产生如同影片断帧那样令人心烦的感觉。为了达到完全高质量的响应效果,上限必须是 30ms。另一方面,如果您确实不介意感觉稍显不连贯,但也不想因为停顿过长而激怒用户,则可按照通常用户所能容忍的限度将该间隔设为 100ms。

这意味着如果想让用户界面保持响应迅速,则任何阻塞操作都应该在辅助线程中执行 — 不管是机械等待某事发生(例如,等待 CD-ROM 启动或者硬盘定位数据),还是等待来自网络的响应。

返回页首返回页首

异步委托调用

在辅助线程中运行代码的最简单方式是使用异步委托调用(所有委托都提供该功能)。委托通常是以同步方式进行调用,即,在调用委托时,只有包装方法返回后该调用才会返回。要以异步方式调用委托,请调用 BeginInvoke 方法,这样会对该方法排队以在系统线程池的线程中运行。调用线程会立即返回,而不用等待该方法完成。这比较适合于 UI 程序,因为可以用它来启动耗时较长的作业,而不会使用户界面反应变慢。

例如,在以下代码中,System.Windows.Forms.MethodInvoker 类型是一个系统定义的委托,用于调用不带参数的方法。

private void StartSomeWorkFromUIThread () {
    // The work we want to do is too slow for the UI
    // thread, so let's farm it out to a worker thread.

    MethodInvoker mi = new MethodInvoker(
        RunsOnWorkerThread);
    mi.BeginInvoke(null, null); // This will not block.
}

// The slow work is done here, on a thread
// from the system thread pool.
private void RunsOnWorkerThread() {
    DoSomethingSlow();
}

如果想要传递参数,可以选择合适的系统定义的委托类型,或者自己来定义委托。MethodInvoker 委托并没有什么神奇之处。和其他委托一样,调用 BeginInvoke 会使该方法在系统线程池的线程中运行,而不会阻塞 UI 线程以便其可执行其他操作。对于以上情况,该方法不返回数据,所以启动它后就不用再去管它。如果您需要该方法返回的结果,则 BeginInvoke 的返回值很重要,并且您可能不传递空参数。然而,对于大多数 UI 应用程序而言,这种“启动后就不管”的风格是最有效的,稍后会对原因进行简要讨论。您应该注意到,BeginInvoke 将返回一个 IAsyncResult。这可以和委托的 EndInvoke 方法一起使用,以在该方法调用完毕后检索调用结果。

还有其他一些可用于在另外的线程上运行方法的技术,例如,直接使用线程池 API 或者创建自己的线程。然而,对于大多数用户界面应用程序而言,有异步委托调用就足够了。采用这种技术不仅编码容易,而且还可以避免创建并非必需的线程,因为可以利用线程池中的共享线程来提高应用程序的整体性能。

返回页首返回页首

线程和控件

Windows 窗体体系结构对线程使用制定了严格的规则。如果只是编写单线程应用程序,则没必要知道这些规则,这是因为单线程的代码不可能违反这些规则。然而,一旦采用多线程,就需要理解 Windows 窗体中最重要的一条线程规则:除了极少数的例外情况,否则都不要在它的创建线程以外的线程中使用控件的任何成员。

本规则的例外情况有文档说明,但这样的情况非常少。这适用于其类派生自 System.Windows.Forms.Control 的任何对象,其中几乎包括 UI 中的所有元素。所有的 UI 元素(包括表单本身)都是从 Control 类派生的对象。此外,这条规则的结果是一个被包含的控件(如,包含在一个表单中的按钮)必须与包含它控件位处于同一个线程中。也就是说,一个窗口中的所有控件属于同一个 UI 线程。实际中,大部分 Windows 窗体应用程序最终都只有一个线程,所有 UI 活动都发生在这个线程上。这个线程通常称为 UI 线程。这意味着您不能调用用户界面中任意控件上的任何方法,除非在该方法的文档说明中指出可以调用。该规则的例外情况(总有文档记录)非常少而且它们之间关系也不大。请注意,以下代码是非法的:

// Created on UI thread
private Label lblStatus;
...
// Doesn't run on UI thread
private void RunsOnWorkerThread() {
    DoSomethingSlow();
    lblStatus.Text = "Finished!";    // BAD!!
}

如果您在 .NET Framework 1.0 版本中尝试运行这段代码,也许会侥幸运行成功,或者初看起来是如此。这就是多线程错误中的主要问题,即它们并不会立即显现出来。甚至当出现了一些错误时,在第一次演示程序之前一切看起来也都很正常。但不要搞错 — 我刚才显示的这段代码明显违反了规则,并且可以预见,任何抱希望于“试运行时良好,应该就没有问题”的人在即将到来的调试期是会付出沉重代价的。

要注意,在明确创建线程之前会发生这样的问题。使用委托的异步调用实用程序(调用它的 BeginInvoke 方法)的任何代码都可能出现同样的问题。委托提供了一个非常吸引人的解决方案来处理 UI 应用程序中缓慢、阻塞的操作,因为这些委托能使您轻松地让此种操作运行在 UI 线程外而无需自己创建新线程。但是由于以异步委托调用方式运行的代码在一个来自线程池的线程中运行,所以它不能访问任何 UI 元素。上述限制也适用于线程池中的线程和手动创建的辅助线程。

返回页首返回页首

在正确的线程中调用控件

有关控件的限制看起来似乎对多线程编程非常不利。如果在辅助线程中运行的某个缓慢操作不对 UI 产生任何影响,用户如何知道它的进行情况呢?至少,用户如何知道工作何时完成或者是否出现错误?幸运的是,虽然此限制的存在会造成不便,但并非不可逾越。有多种方式可以从辅助线程获取消息,并将该消息传递给 UI 线程。理论上讲,可以使用低级的同步原理和池化技术来生成自己的机制,但幸运的是,因为有一个以 Control 类的 Invoke 方法形式存在的解决方案,所以不需要借助于如此低级的工作方式。

Invoke 方法是 Control 类中少数几个有文档记录的线程规则例外之一:它始终可以对来自任何线程的 Control 进行 Invoke 调用。Invoke 方法本身只是简单地携带委托以及可选的参数列表,并在 UI 线程中为您调用委托,而不考虑 Invoke 调用是由哪个线程发出的。实际上,为控件获取任何方法以在正确的线程上运行非常简单。但应该注意,只有在 UI 线程当前未受到阻塞时,这种机制才有效 — 调用只有在 UI 线程准备处理用户输入时才能通过。从不阻塞 UI 线程还有另一个好理由。Invoke 方法会进行测试以了解调用线程是否就是 UI 线程。如果是,它就直接调用委托。否则,它将安排线程切换,并在 UI 线程上调用委托。无论是哪种情况,委托所包装的方法都会在 UI 线程中运行,并且只有当该方法完成时,Invoke 才会返回。

Control 类也支持异步版本的 Invoke,它会立即返回并安排该方法以便在将来某一时间在 UI 线程上运行。这称为 BeginInvoke,它与异步委托调用很相似,与委托的明显区别在于,该调用以异步方式在线程池的某个线程上运行,然而在此处,它以异步方式在 UI 线程上运行。实际上,Control 的 Invoke、BeginInvoke 和 EndInvoke 方法,以及 InvokeRequired 属性都是 ISynchronizeInvoke 接口的成员。该接口可由任何需要控制其事件传递方式的类实现。

由于 BeginInvoke 不容易造成死锁,所以尽可能多用该方法;而少用 Invoke 方法。因为 Invoke 是同步的,所以它会阻塞辅助线程,直到 UI 线程可用。但是如果 UI 线程正在等待辅助线程执行某操作,情况会怎样呢?应用程序会死锁。BeginInvoke 从不等待 UI 线程,因而可以避免这种情况。

现在,我要回顾一下前面所展示的代码片段的合法版本。首先,必须将一个委托传递给 Control 的 BeginInvoke 方法,以便可以在 UI 线程中运行对线程敏感的代码。这意味着应该将该代码放在它自己的方法中,如图 3 所示。一旦辅助线程完成缓慢的工作后,它就会调用 Label 中的 BeginInvoke,以便在其 UI 线程上运行某段代码。通过这样,它可以更新用户界面。

返回页首返回页首

包装 Control.Invoke

虽然图 3中的代码解决了这个问题,但它相当繁琐。如果辅助线程希望在结束时提供更多的反馈信息,而不是简单地给出“Finished!”消息,则 BeginInvoke 过于复杂的使用方法会令人生畏。为了传达其他消息,例如“正在处理”、“一切顺利”等等,需要设法向 UpdateUI 函数传递一个参数。可能还需要添加一个进度栏以提高反馈能力。这么多次调用 BeginInvoke 可能导致辅助线程受该代码支配。这样不仅会造成不便,而且考虑到辅助线程与 UI 的协调性,这样设计也不好。对这些进行分析之后,我们认为包装函数可以解决这两个问题,如图 4 所示。

ShowProgress 方法对将调用引向正确线程的工作进行封装。这意味着辅助线程代码不再担心需要过多关注 UI 细节,而只要定期调用 ShowProgress 即可。请注意,我定义了自己的方法,该方法违背了“必须在 UI 线程上进行调用”这一规则,因为它进而只调用不受该规则约束的其他方法。这种技术会引出一个较为常见的话题:为什么不在控件上编写公共方法呢(这些方法记录为 UI 线程规则的例外)?

刚好 Control 类为这样的方法提供了一个有用的工具。如果我提供一个设计为可从任何线程调用的公共方法,则完全有可能某人会从 UI 线程调用这个方法。在这种情况下,没必要调用 BeginInvoke,因为我已经处于正确的线程中。调用 Invoke 完全是浪费时间和资源,不如直接调用适当的方法。为了避免这种情况,Control 类将公开一个称为 InvokeRequired 的属性。这是“只限 UI 线程”规则的另一个例外。它可从任何线程读取,如果调用线程是 UI 线程,则返回假,其他线程则返回真。这意味着我可以按以下方式修改包装:

public void ShowProgress(string msg, int percentDone) {
    if (InvokeRequired) {
        // As before
        ...
    } else {
        // We're already on the UI thread just
        // call straight through.
        UpdateUI(this, new MyProgressEvents(msg,
            PercentDone));
    }
}

ShowProgress 现在可以记录为可从任何线程调用的公共方法。这并没有消除复杂性 — 执行 BeginInvoke 的代码依然存在,它还占有一席之地。不幸的是,没有简单的方法可以完全摆脱它。

返回页首返回页首

锁定

任何并发系统都必须面对这样的事实,即,两个线程可能同时试图使用同一块数据。有时这并不是问题 — 如果多个线程在同一时间试图读取某个对象中的某个字段,则不会有问题。然而,如果有线程想要修改该数据,就会出现问题。如果线程在读取时刚好另一个线程正在写入,则读取线程有可能会看到虚假值。如果两个线程在同一时间、在同一个位置执行写入操作,则在同步写入操作发生之后,所有从该位置读取数据的线程就有可能看到一堆垃圾数据。虽然这种行为只在特定情况下才会发生,读取操作甚至不会与写入操作发生冲突,但是数据可以是两次写入结果的混加,并保持错误结果直到下一次写入值为止。为了避免这种问题,必须采取措施来确保一次只有一个线程可以读取或写入某个对象的状态。

防止这些问题出现所采用的方式是,使用运行时的锁定功能。C# 可以让您利用这些功能、通过锁定关键字来保护代码(Visual Basic 也有类似构造,称为 SyncLock)。规则是,任何想要在多个线程中调用其方法的对象在每次访问其字段时(不管是读取还是写入)都应该使用锁定构造。例如,请参见图 5

锁定构造的工作方式是:公共语言运行库 (CLR) 中的每个对象都有一个与之相关的锁,任何线程均可获得该锁,但每次只能有一个线程拥有它。如果某个线程试图获取另一个线程已经拥有的锁,那么它必须等待,直到拥有该锁的线程将锁释放为止。C# 锁定构造会获取该对象锁(如果需要,要先等待另一个线程利用它完成操作),并保留到大括号中的代码退出为止。如果执行语句运行到块结尾,该锁就会被释放,并从块中部返回,或者抛出在块中没有捕捉到的异常。

请注意,MoveBy 方法中的逻辑受同样的锁语句保护。当所做的修改比简单的读取或写入更复杂时,整个过程必须由单独的锁语句保护。这也适用于对多个字段进行更新 — 在对象处于一致状态之前,一定不能释放该锁。如果该锁在更新状态的过程中释放,则其他线程也许能够获得它并看到不一致状态。如果您已经拥有一个锁,并调用一个试图获取该锁的方法,则不会导致问题出现,因为单独线程允许多次获得同一个锁。对于需要锁定以保护对字段的低级访问和对字段执行的高级操作的代码,这非常重要。MoveBy 使用 Position 属性,它们同时获得该锁。只有最外面的锁阻塞完成后,该锁才会恰当地释放。

对于需要锁定的代码,必须严格进行锁定。稍有疏漏,便会功亏一篑。如果一个方法在没有获取对象锁的情况下修改状态,则其余的代码在使用它之前即使小心地锁定对象也是徒劳。同样,如果一个线程在没有事先获得锁的情况下试图读取状态,则它可能读取到不正确的值。运行时无法进行检查来确保多线程代码正常运行。

返回页首返回页首

死锁

锁是确保多线程代码正常运行的基本条件,即使它们本身也会引入新的风险。在另一个线程上运行代码的最简单方式是,使用异步委托调用(请参见图 6)。

如果曾经调用过 Foo 的 CallBar 方法,则这段代码会慢慢停止运行。CallBar 方法将获得 Foo 对象上的锁,并直到 BarWork 返回后才释放它。然后,BarWork 使用异步委托调用,在某个线程池线程中调用 Foo 对象的 FooWork 方法。接下来,它会在调用委托的 EndInvoke 方法前执行一些其他操作。EndInvoke 将等待辅助线程完成,但辅助线程却被阻塞在 FooWork 中。它也试图获取 Foo 对象的锁,但锁已被 CallBar 方法持有。所以,FooWork 会等待 CallBar 释放锁,但 CallBar 也在等待 BarWork 返回。不幸的是,BarWork 将等待 FooWork 完成,所以 FooWork 必须先完成,它才能开始。结果,没有线程能够进行下去。

这就是一个死锁的例子,其中有两个或更多线程都被阻塞以等待对方进行。这里的情形和标准死锁情况还是有些不同,后者通常包括两个锁。这表明如果有某个因果性(过程调用链)超出线程界限,就会发生死锁,即使只包括一个锁!Control.Invoke 是一种跨线程调用过程的方法,这是个不争的重要事实。BeginInvoke 不会遇到这样的问题,因为它并不会使因果性跨线程。实际上,它会在某个线程池线程中启动一个全新的因果性,以允许原有的那个独立进行。然而,如果保留 BeginInvoke 返回的 IAsyncResult,并用它调用 EndInvoke,则又会出现问题,因为 EndInvoke 实际上已将两个因果性合二为一。避免这种情况的最简单方法是,当持有一个对象锁时,不要等待跨线程调用完成。要确保这一点,应该避免在锁语句中调用 Invoke 或 EndInvoke。其结果是,当持有一个对象锁时,将无需等待其他线程完成某操作。要坚持这个规则,说起来容易做起来难。

在检查代码的 BarWork 时,它是否在锁语句的作用域内并不明显,因为在该方法中并没有锁语句。出现这个问题的唯一原因是 BarWork 调用自 Foo.CallBar 方法的锁语句。这意味着只有确保正在调用的函数并不拥有锁时,调用 Control.Invoke 或 EndIn-voke 才是安全的。对于非私有方法而言,确保这一点并不容易,所以最佳规则是,根本不调用 Control.Invoke 和 EndInvoke。这就是为什么“启动后就不管”的编程风格更可取的原因,也是为什么 Control.BeginInvoke 解决方案通常比 Control.Invoke 解决方案好的原因。

有时候除了破坏规则别无选择,这种情况下就需要仔细严格地分析。但只要可能,在持有锁时就应该避免阻塞,因为如果不这样,死锁就难以消除。

返回页首返回页首

使其简单

如何既从多线程获益最大,又不会遇到困扰并发代码的棘手错误呢?如果提高的 UI 响应速度仅仅是使程序时常崩溃,那么很难说是改善了用户体验。大部分在多线程代码中普遍存在的问题都是由要一次运行多个操作的固有复杂性导致的,这是因为大多数人更善于思考连续过程而非并发过程。通常,最好的解决方案是使事情尽可能简单。

UI 代码的性质是:它从外部资源接收事件,如用户输入。它会在事件发生时对其进行处理,但却将大部分时间花在了等待事件的发生。如果可以构造辅助线程和 UI 线程之间的通信,使其适合该模型,则未必会遇到这么多问题,因为不会再有新的东西引入。我是这样使事情简单化的:将辅助线程视为另一个异步事件源。如同 Button 控件传递诸如 Click 和 MouseEnter 这样的事件,可以将辅助线程视为传递事件(如 ProgressUpdate 和 WorkComplete)的某物。只是简单地将这看作一种类比,还是真正将辅助对象封装在一个类中,并按这种方式公开适当的事件,这完全取决于您。后一种选择可能需要更多的代码,但会使用户界面代码看起来更加统一。不管哪种情况,都需要 Control.BeginInvoke 在正确的线程上传递这些事件。

对于辅助线程,最简单的方式是将代码编写为正常顺序的代码块。但如果想要使用刚才介绍的“将辅助线程作为事件源”模型,那又该如何呢?这个模型非常适用,但它对该代码与用户界面的交互提出了限制:这个线程只能向 UI 发送消息,并不能向它提出请求。

例如,让辅助线程中途发起对话以请求完成结果需要的信息将非常困难。如果确实需要这样做,也最好是在辅助线程中发起这样的对话,而不要在主 UI 线程中发起。该约束是有利的,因为它将确保有一个非常简单且适用于两线程间通信的模型 — 在这里简单是成功的关键。这种开发风格的优势在于,在等待另一个线程时,不会出现线程阻塞。这是避免死锁的有效策略。

图 7 显示了使用异步委托调用以在辅助线程中执行可能较慢的操作(读取某个目录的内容),然后将结果显示在 UI 上。它还不至于使用高级事件语法,但是该调用确实是以与处理事件(如单击)非常相似的方式来处理完整的辅助代码。

返回页首返回页首

取消

前面示例所带来的问题是,要取消操作只能通过退出整个应用程序实现。虽然在读取某个目录时 UI 仍然保持迅速响应,但由于在当前操作完成之前程序将禁用相关按钮,所以用户无法查看另一个目录。如果试图读取的目录是在一台刚好没有响应的远程机器上,这就很不幸,因为这样的操作需要很长时间才会超时。

要取消一个操作也比较困难,尽管这取决于怎样才算取消。一种可能的理解是“停止等待这个操作完成,并继续另一个操作。”这实际上是抛弃进行中的操作,并忽略最终完成时可能产生的后果。对于当前示例,这是最好的选择,因为当前正在处理的操作(读取目录内容)是通过调用一个阻塞 API 来执行的,取消它没有关系。但即使是如此松散的“假取消”也需要进行大量工作。如果决定启动新的读取操作而不等待原来的操作完成,则无法知道下一个接收到的通知是来自这两个未处理请求中的哪一个。

支持取消在辅助线程中运行的请求的唯一方式是,提供与每个请求相关的某种调用对象。最简单的做法是将它作为一个 Cookie,由辅助线程在每次通知时传递,允许 UI 线程将事件与请求相关联。通过简单的身份比较(参见图 8),UI 代码就可以知道事件是来自当前请求,还是来自早已废弃的请求。

如果简单抛弃就行,那固然很好,不过您可能想要做得更好。如果辅助线程执行的是进行一连串阻塞操作的复杂操作,那么您可能希望辅助线程在最早的时机停止。否则,它可能会继续几分钟的无用操作。在这种情况下,调用对象需要做的就不止是作为一个被动 Cookie。它至少还需要维护一个标记,指明请求是否被取消。UI 可以随时设置这个标记,而辅助线程在执行时将定期测试这个标记,以确定是否需要放弃当前工作。

对于这个方案,还需要做出几个决定:如果 UI 取消了操作,它是否要等待直到辅助线程注意到这次取消?如果不等待,就需要考虑一个争用条件:有可能 UI 线程会取消该操作,但在设置控制标记之前辅助线程已经决定传递通知了。因为 UI 线程决定不等待,直到辅助线程处理取消,所以 UI 线程有可能会继续从辅助线程接收通知。如果辅助线程使用 BeginInvoke 异步传递通知,则 UI 甚至有可能收到多个通知。UI 线程也可以始终按与“废弃”做法相同的方式处理通知 — 检查调用对象的标识并忽略它不再关心的操作通知。或者,在调用对象中进行锁定并决不从辅助线程调用 BeginInvoke 以解决问题。但由于让 UI 线程在处理一个事件之前简单地对其进行检查以确定是否有用也比较简单,所以使用该方法碰到的问题可能会更少。

请查看“代码下载”(本文顶部的链接)中的 AsyncUtils,它是一个有用的基类,可为基于辅助线程的操作提供取消功能。图 9 显示了一个派生类,它实现了支持取消的递归目录搜索。这些类阐明了一些有趣的技术。它们都使用 C# 事件语法来提供通知。该基类将公开一些在操作成功完成、取消和抛出异常时出现的事件。派生类对此进行了扩充,它们将公开通知客户端搜索匹配、进度以及显示当前正在搜索哪个目录的事件。这些事件始终在 UI 线程中传递。实际上,这些类并未限制为 Control 类 — 它们可以将事件传递给实现 ISynchronizeInvoke 接口的任何类。图 10 是一个示例 Windows 窗体应用程序,它为 Search 类提供一个用户界面。它允许取消搜索并显示进度和结果。

返回页首返回页首

程序关闭

某些情况下,可以采用“启动后就不管”的异步操作,而不需要其他复杂要求来使操作可取消。然而,即使用户界面不要求取消,有可能还是需要实现这项功能以使程序可以彻底关闭。

当应用程序退出时,如果由线程池创建的辅助线程还在运行,则这些线程会被终止。终止是简单粗暴的操作,因为关闭甚至会绕开任何还起作用的 Finally 块。如果异步操作执行的某些工作不应该以这种方式被打断,则必须确保在关闭之前这样的操作已经完成。此类操作可能包括对文件执行的写入操作,但由于突然中断后,文件可能被破坏。

一种解决办法是创建自己的线程,而不用来自辅助线程池的线程,这样就自然会避开使用异步委托调用。这样,即使主线程关闭,应用程序也会等到您的线程退出后才终止。System.Threading.Thread 类有一个 IsBackground 属性可以控制这种行为。它默认为 false,这种情况下,CLR 会等到所有非背景线程都退出后才正常终止应用程序。然而,这会带来另一个问题,因为应用程序挂起时间可能会比您预期的长。窗口都关闭了,但进程仍在运行。这也许不是个问题。如果应用程序只是因为要进行一些清理工作才比正常情况挂起更长时间,那没问题。另一方面,如果应用程序在用户界面关闭后还挂起几分钟甚至几小时,那就不可接受了。例如,如果它仍然保持某些文件打开,则可能妨碍用户稍后重启该应用程序。

最佳方法是,如果可能,通常应该编写自己的异步操作以便可以将其迅速取消,并在关闭应用程序之前等待所有未完成的操作完成。这意味着您可以继续使用异步委托,同时又能确保关闭操作彻底且及时。

返回页首返回页首

错误处理

在辅助线程中出现的错误一般可以通过触发 UI 线程中的事件来处理,这样错误处理方式就和完成及进程更新方式完全一样。因为很难在辅助线程上进行错误恢复,所以最简单的策略就是让所有错误都为致命错误。错误恢复的最佳策略是使操作完全失败,并在 UI 线程上执行重试逻辑。如果需要用户干涉来修复造成错误的问题,简单的做法是给出恰当的提示。

AsyncUtils 类处理错误以及取消。如果操作抛出异常,该基类就会捕捉到,并通过 Failed 事件将异常传递给 UI。

返回页首返回页首

小结

谨慎地使用多线程代码可以使 UI 在执行耗时较长的任务时不会停止响应,从而显著提高应用程序的反应速度。异步委托调用是将执行速度缓慢的代码从 UI 线程迁移出来,从而避免此类间歇性无响应的最简单方式。

Windows Forms Control 体系结构基本上是单线程,但它提供了实用程序以将来自辅助线程的调用封送返回至 UI 线程。处理来自辅助线程的通知(不管是成功、失败还是正在进行的指示)的最简单策略是,以对待来自常规控件的事件(如鼠标单击或键盘输入)的方式对待它们。这样可以避免在 UI 代码中引入新的问题,同时通信的单向性也不容易导致出现死锁。

有时需要让 UI 向一个正在处理的操作发送消息。其中最常见的是取消一个操作。通过建立一个表示正在进行的调用的对象并维护由辅助线程定期检查的取消标记可实现这一目的。如果用户界面线程需要等待取消被认可(因为用户需要知道工作已确实终止,或者要求彻底退出程序),实现起来会有些复杂,但所提供的示例代码中包含了一个将所有复杂性封装在内的基类。派生类只需要执行一些必要的工作、周期性测试取消,以及要是因为取消请求而停止工作,就将结果通知基类。

相关文章请参阅:
Applied Microsoft .NET Framework Programming by Jeffrey Richter (Microsoft Press, 2002)
Essential .NET, Volume 1: The Common Language Runtime by Don Box (Addison Wesley Professional, 2002)
有关背景信息,请参阅:
Safe, Simple Multithreading in Windows Forms
A Second Look at Windows Forms Multithreading

Ian Griffiths 是一位专门从事 .NET Windows 窗体应用程序的独立顾问,现居英国。他是 DevelopMentor 的一名讲师,并与他人合著有 NET Windows Forms in a Nutshell (O’Reilly, 2002) 和 Mastering Visual Studio .NET (O’Reilly, February 2003) 一书。

2006年09月13日

在台湾,大部分的人都有个认知,就是"搞硬件的比搞软件的要好赚","做SA/SD比coding
地位要高".那些做ID Design的家伙待的公司所发的股票,永远比搞Software Design的人
待的公司发的还多,股价也高出好几倍. 搞SA/SD的人,在公司地位好象也比单纯写程序的
人高.于是我的父亲有一次问我:"是不是搞硬件的人智商比较高?","是不是搞分析设计的
人比较有地位?"

事实上,在学生时期我就认识不少人,有的人因为程序写不过别人,所以转换跑道专攻硬件
;也有人觉得每天面对示波器和逻辑分析仪让他们头痛,工程数学也学不过人家,所以转行
搞软件.到了实际上面对许多前来接受教育训练的学员之后,发现有人是因为写了十几年
BIOS和driver,觉得乏味而没有前途,所以想试看看应用程序的开发;有人则是因为受不了
客户每天改需求,而且写BIOS和driver的人好象比较少,价格比较高,因此想学习如何撰写
系统软件. 询问那些上高阶课程的朋友,有的人发现自己只会胡做SA/SD唬烂老板和客户
,所以希望重新打好基础,从基本的coding做起;有的人则因为写程序写太久,觉得应该往
上走向SA/SD的路子,毕竟老了程序就写不动了.

这个世界上什么样的人都有,绝对不能以小现大.就很像搞政治的老是骗我们台湾只分成
外省人和本省人,搞帮派的人告诉我们警察是带着枪的流氓.实际上,每个族群大家在没有
被分化之前都相处的和乐融融,而虽然有警察掳妓勒索,上班时间跑去聚赌,但是也有开我
们罚单时酌情轻罚的警察,和保护我们身家性命的警察.

即使如此,身为一个程序设计师还是要问,那为什么写程序的好象地位就是比较低?笔者认
为,这是进入门槛的问题.会做相同事情的人多了,会会削弱那样技能的价值. 就很像考过
MCSE/MCSD证照的人,通常比起考过SCJP/SCJD证照的人要来的没价值,前者因为考过的人
实在太多,而后者考过的人少,仅是如此而已.但是绝不是代表考过MCSE的人素质就比考过
SCJP的人要差,也不代表考过SCJP的人实力真的比较高段.

相信求学的时候您一定遇过一种人,他实力也不怎样样,可是考试的时候他x的就是考的比
我们高,怎么我们心里就是不服,可是大多数老师偏偏就是比较喜欢分数高的学生.

因此,满坑满谷的人自称会写程序,老板不愁找不到可以做project的人,你要求的薪水和
价格很高? 没关系,找个学校刚毕业的小子,写的比你快,写的比你好,用的技术比你新,更
重要的,价格比你这个老屁股便宜一半.所以程序设计师在雇主心中的地位日益低落,自然
有迹可循.

那么,为什么写程序这个行业的进入门槛比较低???

在各位信息从业人员的身边,非科班出生的比率非常高(当然没还卖鸡排的高),
科班出生的学生,搞不好在老板眼中的地位,还比不上一个半路出家的自学者. 那位自学
者靠着自己的天份,学会SP,JSP,PHP,C++,C+,Java,Linux,Windows,Oracle…等,十八般
武艺样样精通.而我们科班出生的学生,只会离散数学,数据结构和算法,Visual Basic搞
不好用的没人家熟,网站的架设搞不好也没人家熟.更何况科班出生的学生,他的基础学问
搞不好根基也不扎实(问看看您身旁的科班学生,和现在大多数灯红酒绿的大学生),难怪
最常见的就是很多非科班的"高手"看不起科班的学生,觉得他们无三小路用,连个Window
s*作都比他们差,Word也没人家会用,甚至还常常听到某些人大言不惭的说:"我这辈子写
程序从没用过书上的数据结构和算法,读那些干啥?". 长期下来,科班的学生也开始质疑
自己的所学,甚至考虑转行.

当然也有曲高和寡,孤芳自赏之人. 但终究阳春白雪还是比不上下里巴人,这个世界上能
够看清现况的人毕竟是少数数. 总之,没办法受到老板垂青,薪水永远那么少,想卖鸡排的
念头就开始萌生.

之所以会有这种情况,笔者个人认为有两种主要原因,一是技术的流通性,二是台湾软件市
场的需求.

为什么技术的流通性可以产生大量半路出家的非科班生? 这都要感谢台湾大量作者,写出
大量的入门书籍,大幅减低的进入程序设计领域的门槛. 即使在各位眼中,繁体中文的烂
书很多,而且比例多的惊人.但是,大家眼中的烂书,常常是再版多次的入门书,对某些人认
为是垃圾的东西,常常是另外一批人进入信息业的最佳踏脚石. 不信各位到书店看看,是
"24小时学C++","快快乐乐学Java"的书比较多,还是"轻轻松松学微分方程式","21天学复
变","电子学不求人"的书多? 两者的差距是 N:0. 是微分方程式和复变比C++,Java难学
吗? 笔者两个都学过,至少我可以发誓,就一个有基本学习能力的人来说,难度是一样的,
学成所需要的时间和功夫也是一样的.

但是,别忘了,最大的问题也是在此. 如果有人可以轻松的就学会别人辛苦学成的学问,那
么那些身处信息大厂那些年薪是我们数倍的优秀程序设计师是白混的吗? 我们承认这个
世界上有天才,但是,人家以是经过努力再努力.不努力的天才,顶多就像周芷若使出的九
阴白骨爪,只徒具形式,不具威力.拼爆发力可能没问题,但凭内力硬干的时候,只就只能像
铁掌帮裘千刃的双胞兄弟一样躲躲藏藏.虽然可以勉强唬过老板和不懂的人.但是你永远
不会进步. 这种"浅碟文化"在我们的周遭有太多例子.

再看我们的公开讨论区(BBS或Web Forum),我们可以看到大量的软件技术讨论区,却看不
到硬件技术讨论区(喔!请别跟笔者说那些每天问哪家主机板比较好,哪台烧录器较稳定,
或者说CPU时脉越高速度就越快越好,或是说x86的Out-Of-Order Executon是"故障执行"
 这一类浑话的讨论区和我说的硬件技术讨论区是一样的).

书籍和讨论区促进知识的流通,造成了大量的软件人才,更何况,学习设计软件只要一台P
C就能写出好用的软件,而学习硬件设计,需要的设备更是天价. 更重要的一点,就是学软
件的人,充分发挥儒家分享的精神,只要不是在BBS上冒充小妹妹想要骗取大哥哥的同情来
帮你写程序作业,只要不是很差劲的乱问一通,通常至少会有热心人士愿意留下一个URL,
让我们可以找到参考文件,更具热诚的,如新竹师院BBS站Java版的TAHO先生,还擅长用很
多生动的比喻让初学者体会技术的真义. 在这种知识充分流通的环境下,当然产生出非常
多的信息从业人员.

每一台PC,上头各种软件的需求是无穷无尽的,每天都有各行各业的人需要各种软件,所以
程序设计师的需求有一定的量. 难怪有人说:"写程序的人饿不死,但是也不会发大财)
再来就是台湾软件市场需求所引发的问题.

一般我们把软件分成系统软件(System Software)和应用软件(Application Software).
 编译器,组译器,除错器,*作系统,驱动程序,都是属于系统软件的一部分.台湾除了硬件
厂商,IC Design House之外,甚少有撰写驱动程序的需求. 前几年Linux被炒的热时,也有
许多高手投入*作系统,编译器以及简化标准函式库的研究.而台湾所需求的软件,极大多
数是属于应用软件,这些需求的应用软件其中很大比例是和数据库有关系的(其实不只是
台湾,其它地方也是约略相同的情形).

所以VB,Delphi和PowerBuilder这类IDE大行其道,因为他们容易上手,可以快速开发数据
库应用程序,相关入门书籍很多,会的人更多. 因此符合我们前面所说:"会的人越多,价值
就越低"的理论.

但是别忘了,系统软件有部分是偏向应用软件的,比方说开发软件用的JBuilder和Visual
 Studio;制图用的AutoCAD;一套IC Design用的Xilinx或Altera;做OOA/OOD的Rational 
Rose和TogetherJ, 笔者习惯称这些软件叫做"软件的软件"(Meta software). 在一般程
序设计师眼中,这类软件"理所当然"地认为都是由国外所发展.要发展这类软件有极高的
进入门槛,所以国外这类软件公司的获利率很高,该公司的程序设计师可是身价非凡. 要
知道,先前在媒体上有报导过,能够让Microsoft买软件来用的公司,只有Rational Rose(
当然,不可能只买Rose来用). 如果做IC Design的人没有你的软件就没办法做事,身为一
个程序设计师,你会觉得你的身价和地位比做IC Design的人低吗? 如果你做的软件像So
ftICE或DriverWorks那样被做硬件的工程师大量地倚赖,你会觉得搞硬件的人比你厉害吗
?

问题是,知道该如何设计一套这类软件的人,恐怕才是真正的异数. 而且需要跨领域的专
才.要不是许多OpenSource的project(例如:KDeveloper)正在进行,恐怕很少人知道该如
何设计一套IDE. 既然会制作这类软件的人在台湾少之又少,那么他们的身价高吗? 很抱
歉,台湾地区没有公司发展出足以和国外大厂竞争的开发工具. 举IDE为例,台湾早期有家
公司开发出一套名为DBtools的产品勉强可以算的上是这类产品,旗标也曾为它出版过入
门手册,可是使用这套软件的人好象…没看过(如果该产品有不错的占有率,请原谅我是
井底之蛙),没有市场,公司无法赚钱,即使你是少数能做制作某类型产品的高手,公司最后
关门大吉了,您也只能算是"少数能让公司赔钱的程序设计师"罢了.所以我大胆假设, Ja
mes Golsing或Anders Hejlsberg如果学李敖一样50年不离开这个小岛,大概早饿死了.

以上的讨论,真正优秀的高手看了之后,或许会产生有时不我予,不得已必须远渡他乡的想
法.程序设计师的价值就如同书的价值一般,台湾地区和国外有不同的看法.台湾地区教科
书比较便宜,应用的书比较贵.国外教科书比较贵,应用的书比较便宜. 国外重内容,薄薄
一本How Debuger Work可以卖44.99美金.而台湾,都是看页数来计价(侯俊杰先生的书有
努力在打破这个页数的迷思,也做的蛮成功).有很多原因导致这两种文化之间的差异,但
是,市场因素肯定是其中重要的一项.

程序设计师价值低落的原因我们讨论过了,如果您同意笔者的论点,那么不禁疑惑,大家彼
此毫不保留地让知识得以流通,技术得以扩散,本意是好的,结果反到造成自己和别人身价
的低落,这样我们是不是从此不再和别人分享我们的心得会比较好?
TAHO先生看过本篇上半部之后,发表了底下内容:
——————————————————–

发信人: TAHO.bbs@bbs.nhctc.edu.tw (痴人)
> 哈 恭喜TAHO兄上Java周报了…
> 通常至少会有热心人士愿意留下一个URL,让我们可以找到参考文件,更
> 具热诚的,如新竹师院BBS站Java版的TAHO先生,还擅用很多生动的比喻让初学者体会
技术的真义。
@@"…..
光看这一段 好象是褒奖
不过看了整篇文章…
突然发现……
原来我是让程序设计师不值钱的凶手之一….
真是罪该万死啊….
——————————————————–
但是mue先生接下来的发言,说却命中本文真正的核心价值观:
——————————————————–
发信人: mue.bbs@bbs.im.tku.edu.tw (闲人)
: @@"…..
: 光看这一段 好象是褒奖
: 不过看了整篇文章…
: 突然发现……
: 原来我是让程序设计师不值钱的凶手之一….
: 真是罪该万死啊….
不会啦..要是每个程序员都死守自己所会的..让所谓的专业门槛更高的话..就很难进步
了..就是让本来难的变简单..自然就会有更难更深入的东西出现..这样社会才进步的快
丫..
——————————————————–
做应用软件的人真的没价值吗? 有些靠应用软件赚了不少钱的朋友开始偷笑了.软件必须
加上专业知识,才能够发挥价值. 换句话说,如果您真的只会"写程序",
却没有配合各种领域的专业知识,"写程序"这项技能本身根本毫无价值可言.
就很像你的计算机装了浏览器,可是却没有对外联机的网络一样,如果没有网络本身,浏览
器一点价值也没有,更不需要一家公司大费周章地利用平台优势去整倒另外一家公司.
我们可以把程序设计师比喻成架子上满满的信息相关书籍,而顾你的老板就是前来买书的
读者.你不能否认这么一堆书里头,有好书,也有烂书,你也不能否认你心中的烂书是一本
书,你更不能否认书里面教你如何写Java程序的内容是错的.可是,购书的读者压根儿根本
觉得某几本书一点价值也没有.
我们只能说,太多只会"写程序"的程序设计师打烂了市场,就像一牛车的烂书一样,你不能
说烂书没有价值,可是烂书会让你对架上其它书籍的价值大打折扣.除非,你之前看到别人
口耳相传这本书是好书.大家或许不相信"只会写程序"的程序设计师存在于这个世界上,
所以笔者举个简单的例子说明之:
很久以前, BBS上有人发表了post,内容大该是说某公司在整理旧的程序代码时,
发现了一则奇文,奇文内容如下:
—————————————————————————-
—–
//底下是一个判对某个整数是不是奇数的程序代码
public static bolean isOdd(int n)
{
while(true)
{
if(n==1) return true;
else if(n==0) return false;
n=n-2;
}
}
—————————————————————————-
—–
您说撰写这个程序代码的人不会写程序,他又好象会写,
你说他会写程序,你又不服气,觉得这个家伙根本在胡搞一通.看过这则奇文轶事之后,当
您下次听到某公司抱怨他们的程序设计师做出来的软件一堆bug,速度出奇的慢,产品永远
卖不出去,害公司快经营不下去了,所以他们认为程序设计师一点价值都没有,只是一堆劳
碌命的杂碎时,您会不会更加怀疑,是一堆莫名其妙的程序设计师,把你的行情给搞烂了?

所以从今天开始,如果你对程序设计有一股热爱,那么我们一起努力,做个真正的程序设计
师,而不要做一个破坏别人行情的程序设计师.做一个真正写的出有用软件的工程师,不要
做一个只会写程序的程序设计师.当然,如"意外的计算机王国 / 联经出版社"一书所言,
很多技术都是用来原本没有预料到的地方而大行其道.许多科学研究无法有立即的贡献,
但是影响深远,我们也期许有意从事基础研究的科班研究生,认真的作研究,不要老是研究
一些别人已经研究过的研究,不要老是冀望骗国科会等研究机构的经费,到了最后计划结
案时,才匆匆忙忙交出另外一篇骗更多钱的计划书,或是拿不出台面的研究.
最后,我们反省自己是不是也是一个只会"写程序"的程序设计师呢? 请自行测验底下几个
问题,这些问题都不可能有客观的答案,所以每个问题都附上笔者主观的答案,作为笔者自
己的反省.

Q1: 你尊重专门技术吗? 换句话说,你认为术业有专攻吗?
当你接受外面的教育训练课程时,你总是崇拜看起来什么都懂的老师?
换句话说,你认为那些遇到课外问题就跟你说他不懂的讲师是烂老师?
你老觉得真正的高手应该精通各门各派的技术,如果你会XML,他不会,你就觉得你比他厉
害.你觉得他的履历上写的技能太少,证照太少,所以你认为你比他优秀?
有人老是觉得自己蛮会用MFC开发软件,所以直觉认为那些只喜欢,或是只会用VB的人程度
应该不高.问题是,有人用VB的程度是,当他觉得组件不好用,所以自己写程序处理HTTP,因
为他懂HTTP协议的运作方式.组件盘里附的浏览器组件太烂,就自己用公认语法不是很顶
尖的Basic语言来写parser.

相反的,有人号称会用MFC,但是除了靠Help找出名为Cxxxxx的类别来用,再
自己补上事件处理的部分之外,其它什么事都做不出来.
有人认为写Java程序应该善用工具,用UltraEdit根本是重新造轮子的行为,所以一开始就
学JBuilder的使用,其实他用JBuilder写了老半天GUI程序,哪天回头叫他用文字编辑器写
个简单的Frame + Button, 他却写不出来,因为他从没弄懂过Java的事件处理模型. 他只
会不断地: 选择组件->放在容器里头->调整位置和大小->调整属性->按两下->填写事件
处理函式,

成为一个名副其实的"程序女工"(再注:女工纯指"经年累月从事重复单调工作者",没有歧
视女性的意思).
有人觉得他精通各家厂商的数据库,所以看不起那些只会下SQL指令或是只会写store pr
ocedure的人, 因为这个人精通ODBC, JDBC, ADO, ADO.NET各种程序的写法.问题是,一个
精通SQL的专家和只会写SQL指令的人,在数据库表格交互参考,资料量很大的时候,要从中
取出我们需要的资料,所下的指令在效率上是几秒钟和几个小时的差别. SQL也是个专门
学问,要能够巧妙的*作它,必须下非常多功夫做研究,而且一研究可能就是十几年才有办
法累积丰富的经验. 如果贵公司的项目老是苦于数据库存取的效能不够,你猜老板会花钱
找一个有能力彻底改善所有SQL命令之中效能问题的稀有专家,还是再找一个号称他什么
都会,结果一点用场也派不上的"数据库女工" ?

我们常常看到某人列出他的履历,好象会很多就是很厉害.但是当我们完全深入
一项技术时(喔,我是说你真正下工夫的时候),通常我们会越来越感觉到自己的渺小.
蔡学镛先生就是一个非常尊重专业技术的例子.
我们看到他在 CSDN专栏http://www.csdn.net/expert/cxy/ )上写的,他说他只精通 l
ots of Java APIs.我和学镛聊过三次,有一次,我听他说:"干麻叫我搞Linux,我又不懂L
inux!" 如果是你听到这句话,你会不会真的以为他玩起Linux来肯定比你逊色?
笔者突然想起神雕侠侣里头的独孤求败,晚年只会拿树枝和别人比武,可是你拿再厉害的
刀剑就是无法打败他.

所以,请尊重专业技术,不要以为人家没说他会,你就比他厉害.真正厉害的人很多都不在
台面上,而是躲在后面偷偷笑我们呢! 而我们一辈子永远不知道我们被别人偷偷取笑了.
中国文化数千年来都是文人相轻的历史,够了,大家尊重专业吧!

Q2: 你觉得算法和数据结构无三小路用,因为你从没使用过?

我们承认"无招胜有招"是内功心法的最高境界,但是在信手拈来之际,后面所代表的是对
各家武功路数的彻底了解.由于台湾几乎只有应用软件的开发需求,没有系统软件的需求
,所以大多数的程序设计师都是站在"程序女工"的角度看世界,只要有钱,只要有人贩售组
件,有什么搞不定的.
但是今天如果你想设计一个XML parser,不懂数据结构和算法可以吗?好吧! 你说我们不
该重新造轮子,我们应该站在巨人的肩膀上看世界,如果什么都自己硬干,世界是会退步的
. 那么试问,当你在使用Java提供的Collection Framework时,你了解ArrayList, Linke
dList, TreeSet, HashSet之间的差别吗? 你知道他们的优缺点吗? 你知道他的特性吗?
 不了解ArrayList和LinkedList的差异,用哪种去写程序执行结果都一样,可是效率差很
多.

大多数的人连了解特性都谈不上,更别说很多每天想发展自己的语言,自己的编译器,自己
的*作系统的人,没有基础学问的了解,如何去设计一个Collection Framework或STL?
你说数据结构和算法没有用,你去做看看现在IDE中普遍有的code insight功能看看?以B
orland C++ Builder来说,要在短时间内搜寻所有的标头文件并找出某函数的prototype
,如果没有对数据结构和算法有充分了解,一样做的出来,只是产品会卖不出去罢了.
我在课堂上常常举一个scalability的例子给学生看:
我希望写一个1+2+3 … + 100的程序,如果撰写此程序是你的工作,大多数的人都是写成
:
int sum = 0 ;
for(int i = 1 ; i < 101 ; i++)
sum = sum + i ;
而真正受到数学观念熏陶的人会写成:
int sum = 100*(100+1) / 2
前者的复杂度是O(n),后者是O(1),当项数很多时,运算时间是不是差很多?这些都是我们
的教育所产生的问题(当然笔者也是其中一位受害者),老师只叫你写好作业,助教只叫你
run出正确的结果,认真一点的还会测试你是不是抄来的. 却从来没告诉你程序中不能只
有一个main函式,程序代码不能第一行写到第一千行从不切割成其它子程序. 你的程序代
码看起来不堪入目,老师助教从没告诉你,你的.class檔被decompiler反编译之后,长的比
你写的还漂亮.但是从今天开始,我们可以开始认真思考每行程序,不要再做一个拖累其它
人行情的程序设计师.

试想发展MP3算法的人和写WinAmp的人,哪个比较厉害?
你会说都很厉害,可是没有前者就没有后者,前者搞不好还可以坐收权利金,后者只能苦哈
哈的赚些小钱或等人购并. 我们停留在崇拜应用程序技巧的阶段,而真正值得崇拜的是那
些难得一见的创意.

笔者遇过一个朋友,叫他撰写一个费式数列的小程序,比请他写一个可以浏览数据库表格
内容的程序还难.(请不要与我讨论费式数列的小程序没有实用价值的问题,这里讨论的重
点不是这个)前者需要稍微动点小脑筋,后者只要会拖拖组件,设定property就搞定.
RAD本身不是罪,但是没学好九阳神功就妄想几小时练成乾坤大挪移.最后只会走火入魔而
死,彻底变成一个"程序女工".

Q3: 你常常以科班和或非科班自居?

你是科班生,瞧不起非科班生? 因为你是正统?
你是非科班生,瞧不起科班生? 因为你觉得会的东西比科班生的还多.
烂学校会出现好学生,好学校也会有烂学生.
因为比例一样多,所以我们不能以偏概全.
如果仗着受过几年正规教育,自己又从未好好深入学习,就自己为是正统,比较学术的说法
这叫做"阳具文化".有些创新的idea是一般制式脑袋的科班学生很难想出来的,因为专家
是训练有素的狗.如果你是学电信的朋友,你发现交换机是一个葬仪社的老板因为生意被
别人抢走而发明的,那你会不会气死?
如果自学有成的程序设计师仗着自己会的东西比较多,你说你精通Java的各种技术,你看
不起从没写过JSP的科班学生.

但是有人告诉你发展Java的James Golsing博士是一个正统出生的科班生,知道了这件事
情,会不会让你更加尊重幕后认真打拼的科班生?
Q4: 你是学计算器科学的,可是逻辑能力并没有比较好,还常常受骗?
你会被潮流所鼓动吗? 你常常被别人的思考牵着走?人家鼓吹Linux多好多好,你的脑袋连
转都没转过就发愤努力地考Linux认证 ?
——————————————————————-

别人把公司里的server全换成Linux,客户端也都改成Linux,公司仍然正常地运作,结果你
学了Linux之后,看到电视上BSA同法务部做的广告仍然吓的你冷汗直留. 深怕明天去住套
房.

Sun跟你说Java跨平台,你没试过也跟人家说跨平台的优点?
—————————————————
Borland已经可以做到一份光盘里同时附上Solaris, Linux, Windows, MacOS X的JBuil
der,你却为了EJB无法deploy到不同公司的Application Server忙的像无头苍蝇.
之前一窝峰人鼓吹XML,结果你盲目追求流行,做出来的东西tag比data还多?
—————————————————————–
会用的人彻底了改变了公司里资料交换的流程,而你整天只会SAX来,DOM去的写XML数据库
(用XML来储存资料的数据库)

微软的广告告诉你XP和IE将不支持Java,你都还没试过就跟别人嚷嚷Java已死?
——————————————————————–
套句BBS上moga先生的名言:"那我现在在Windows 2000上跑的Java程序是神迹?"
现在一票人每天宣传web service的好处,你连想都没想过就急着想要把公司的旧系统全
部改成web service来做,结果浪费一堆钱,糟糕的速度让你每天被客户臭骂 ?
———————————————————————-
Web service当然是美好的前景,但是并非适用于每个角落,目前世界上并不存在完美的s
olution.

人家说不能写程序一辈子,写程序的人生命周期很短,你也跟着别人开始往SA/SD前进 ?
—————————————————————————
如果世界上每个工程师都可以经由经验就成为优秀的SA/SD人员,那么理论上咱们应该有
些象样的软件产品才对. 有些人写了几十年,还是一个优秀的程序设计师,你问问他,如果
没有遇到糟糕的老板,糟糕的待遇和糟糕的制度,他愿不愿意写一辈子程序? 我愿意.
顾问告诉你要多用RAD,不该重新造轮子,所以你努力的问how而不问why ?

—————————————————————
结果真正赚到钱的都是那些像JReport做软件组件的软件公司.
微软说J2EE Blueprint的Pet Store,用.NET技术做比用J2EE做还要快许多,然后你就相信
了,最近, IBM和Oracle重新加强Java版的Pet Store,让它比.NET版的还要快18%~22%,你
又改口说Java比较好.

—————————————————————————-
—————-
一个系统在设计的时候有很多考量,有人以扩充性为主,有的以安全性为主,有人以效能为
主.
如果没有设计理念,大家程序里头的function全部改成inline就好了,管他编译出来的执
行档有多大.
系统只以效能做考量,我们还需要Design Pattern做什么?

那些王八蛋数据和我们选举时的民调一模一样…对一个脑袋清楚的人完全没有参考的价
值.

当然,以情感因素来看民调的人例外.
如果你没有经过自己的自主判断就盲目的跟随潮流,那么下次当你看到有人排队买米酒,
买蛋塔,抢购卫生纸的时候,请不要投以排队的人们奇怪的眼光.

Q5:你尊重老前辈吗?

我们都相信,世界上唯一不用努力就可以获的东西就是老.
所以吃过的盐巴比你吃过的米还多的人,没有任何值得尊重的.
我们更相信,信息业永远是年轻人出头,而英雄少年也常在心里想:
"李杜诗篇万口传,至今已觉不新鲜,江山代有才人出,各领风骚数百年."
但是我们认真想想,从Apple 2的时代到现在随便一颗CPU都是1 GHz的时代,
计算器的本质有什么改变吗? 不就是一台不断对内存作处理和I/O动作的机器.
你笑那些只会用Fortran或COBOL的老前辈,那你学的Java或C#比起这些老语言又高明到哪
里去?

写程序不过是 宣告,循环和函式三大要素.时间久了,产生了一堆新名词,配上一些新的发
展理念但本质上没有改变.

如果你是推倒前浪的后浪,当你看到李维先生撰写的 "[长篇] 我的回忆和有趣的故事"或
侯捷老师最近两期在Run!PC撰写的"侯捷观点"这些老前辈写的文章,你有把握写出比它们
更高明,更有深度的东西吗?
最重要的问题,

Q6:你骗过老板吗?

你在履历上写的十八般武艺样样精通,结果是梧鼠技穷(注:比喻技能虽多,而不能专一)
,一录取之后什么东西都做不出来,笔者至少听过20个老板跟我讲过这件事情.你以为你的
身价比较高,只因为你从事"软件研发"的工作?

然而现实的生活中,"获利"是真正决定成败的关键.除非你的东西帮老板赚了钱.
如果没有,你凭什么要求更多薪水,凭什么要求50张价值数千万元的股票?如果你写的东西
品质很差,bug超多,客户抱怨不断,老板赚不到很多钱,你还老是在外头痛骂老板不尊重技
术人员.如果把行业换成色情行业,那老板不就等于被干洗? 这样看来,程序设计师和詹惠
华(黄显洲3P案女主角)干的事情有什么两样?如果这样的杂碎程序设计师太多了,真正要
去卖香鸡排的,不是写程序的人,而是雇用了这些杂碎的老板才对. 不过很遗憾,真的很多
软件公司的老板要改行了,鸡排太多人卖了,我建议某位感同身受老板写篇"程序员的老板
与蚵仔煎",请踊跃投稿.

2006年09月08日
一.概要
Windows XP开创了一种全新的Windows用户界面(UI),这种用户界面给用户一种现代化的、超时髦的感受。Windows XP新创了闪亮的具有圆形边角的控件以及极富未来派气息的进度条等具有新风格的控件。
而现在的Visual Studio.Net也具备了此类控件。那么开发者就想能否在自己开发的程序中使用这类超酷的控件呢。答案是肯定的,程序员所要做的仅仅是添加一些引用以及一个资源文件。本文就通过介绍一个实例向大家介绍如何在Visual Basic和Visual C#下将控件的Windows XP风格应用到自己的程序中。
注:本文介绍的控件风格特性只能在Windows XP下的运用程序中实现。
二.介绍
我们先来做一个有关控件风格的比较。
在Windows XP下的控件具有一种全新的外观。图示如下:
而在Visual Studio.Net中提供的控件虽然和Windows XP下的是同样的,但是它们的外观却截然不同。图示如下:
本文就向大家介绍如何使Visual Studio中的控件和Windows XP下的控件一样,具有超酷的外观、良好的用户界面。
你可以认为一个窗体由两个相互独立的部分组成:一个客户区以及一个非客户区。所有在Windows XP操作系统上运行的程序都有一个非客户区,它包括:窗体框架、标题栏以及的非客户区的滚动条。操作系统会自动给非客户区应用Windows XP风格,所以尽管什么也没做,你也可以看到自己的程序在Windows XP上运行时具有新风格的窗体框架、标题栏以及滚动条。而我们真正要做的就是使客户区的控件也具有Windows XP的风格。
三.实现原理
非客户区的外观是由当前所应用的视觉风格决定的。一个运用程序或是操作系统的视觉风格是可以被更改的。就像上面提到的那样,当一个运用程序运行在Windows XP上时,窗体的滚动条以及标题栏就立即改变了外观风格。只要运用程序应用了版本为6.0的Comctl32.dll,那么其中的某些控件就是自动的呈现新的外观。
这类控件如下:
·                     TextBox控件
·                     ListView控件
·                     RichTextBox控件
·                     TreeView控件
·                     HScrollBar控件
·                     DateTimePicker控件
·                     VScrollBar控件
·                     MonthCalendar控件
·                     ProgressBar控件
·                     Splitter控件
·                     TabControl控件
·                     TrackBar控件
·                     MainMenu控件
·                     StatusBar控件
·                     ContextMenu控件
·                     ToolBar控件
·                     ComboBox控件
·                     TreeView控件
·                     DataGrid控件
·                     ListView控件
·                     ListBox控件
 
其他的控件则需要一定的条件。确切地说,从System.Windows.Forms.ButtonBase类继承过来的控件(Button,RadioButton,GroupBox以及CheckBox等控件)有一个FlatStyle属性。这个属性表明控件应该先被绘制。通过设置这个属性,控件可以用以下几种方式来绘制:
属性
描述
Flat
控件为平坦的
Popup
鼠标在控件上时,控件为三维的,否则为平坦的
Standard
控件为三维的.
System
控件的外观由用户的操作系统设置决定
你可以发现,当FlatStyle属性被设置为System后,控件的外观就由用户的操作系统设置所决定。这样的话,要是用户的操作系统为Windows XP,那么相应控件的外观就会呈现Windows XP的风格了。
当FlatStyle属性被设置为System后,能改变视觉风格的控件如下:
Button 控件
RadioButton 控件
CheckBox 控件
GroupBox 控件
最后,还有一些控件在Windows XP和Visual Studio下是一样的,这些控件如下:
Label 控件
LinkLabel 控件
DomainUpDown 控件
NumericUpDown 控件
CheckedListBox 控件
四.使用Manifest文件
如果你想在你的运用程序中运用Windows XP的外观效果,你必须给你的工程添加一个Manifest文件(在建立工程过程中用来确定资源的一个文件)。这个文件指明了在工程中应用版本为6.0的Comctl32.dll文件(只要这个文件存在)。版本为6.0的Comctl32.dll文件包括了一些新的控件以及一些控件的新特性,它和以前版本的最大的差异就是它支持控件外观效果的改变。
不像以前的版本,版本为6.0的Comctl32.dll是不可以被重新发布的。你只可以在包含它的操作系统中使用它的动态连接库(DLL)。Windows XP既包含了版本为5.0的,又包含了版本为6.0的(在默认的情况下,运用程序是用版本为5.0的Comctl32.dll的)。在版本为6.0的Comctl32.dll中,包含了用户控件和一般控件。你只要改变和这些控件相关的dll文件,就可以使它们呈现出Winodws XP的外观风格了。
为了和用户的计算机操作系统相协调,你必须在你的运用程序中建立一个Manifest文件来明确的指定其窗体控件使用的是版本为6.0的Comctl32.dll。该Manifest文件是一个XML文件,它包含在你的程序中,作为一个资源,或是在可执行文件目录下的一个单独的文件。
因此,为了使你的运用程序具有像Windows XP那样的外观效果,你必须:
1.若控件有FlatStyle属性,则把它设置为FlatStyle.System
2.建立一个Manifest文件,将版本为6.0的Comctl32.dll捆绑到你的运用程序中(下面的例子中的Manifest文件可以将该Comctl32.dll捆绑到任何用Visual Studio.Net建立的运用程序中)
3.把这个资源(Manifest文件)添加到你的可执行文件并进行重建
五.将Windows XP视觉风格应用到控件中
学会应用Windows XP视觉风格的最简单的方法就是学习做一个实例。本文最后就向大家介绍任何建立一个简单的运用程序并使它的窗体上的控件具有Windows XP视觉风格。
接下来,我们要做的就是:
1.建立一个运用程序的工程,并在窗体上添加一些控件
2.建立一个Manifest文件,将所需的DLL捆绑到你的运用程序中
3.将该Manifest文件存放在可执行文件目录下
4.添加一项资源(Manifest文件)到可执行文件
下面就开始创建新的工程……
创建工程:
1.新建一个Windows运用程序工程。(注:请记住工程名以及工程存放的目录,下面有用)
2.从工具箱里拖以下控件到窗体上并将它们排列好:
Button 控件
RadioButton 控件
ProgressBar 控件
CheckBox 控件
Label 控件
(注:虽然Label控件的外观不会改变,它也被拖放到窗体上以作对比)
3.将Button,RadioButton,CheckBox等控件的FlatStyle属性设置为System。(技巧:你可以在点击每个控件的同时按下Ctrl键,这样就可以同时选中这三个控件。然后,在FlatStyle属性的下拉框中选定System即可)
4.双击Button控件以给它添加事件处理函数,代码编辑器会自动打开。
5.添加以下一些代码,设置ProgressBar控件的Value属性,那样你就可以看到新的一个进度条了:
‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Value = 50
End Sub
// C#
private void button1_Click(object sender, System.EventArgs e)
{
progressBar1.Value = 50;
}
6.从“生成”菜单上选择“生成”选项。
7.最后,全部保存。
建立Manifest文件:
建立一个XML文件,将正确的版本的Comctl32.dll捆绑到你的运用程序中。
新建并编辑Manifest文件:
1.在解决方案资源管理器中,右击工程名:添加->添加新项
2.在添加新项对话框中完成以下工作:
A.在左边的方块中点击“本地项目项”。
B.在右边的方块中选定“文本文件”。
C.在名称框中以下面的方式命名文件:<Executable Name>.exe.manifest。因此,如果你的运用程序名为MyXPApp,那么你应该将这个XML文件命名为MyXPApp.exe.manifest。
3.点击“打开”按钮,文本编辑器中打开了你新建的文件。
4.将下面的XML添加到该文本文件:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="X86"
name="Microsoft.Winweb.<Executable Name>"
type="win32"
/>
<description>.NET control deployment tool</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
5.将上面第五行中的<Executable Name>替换成你的运用程序名即可。
6.从“生成”菜单上选择“生成”选项。
7.最后,全部保存。
将该Manifest文件存放在可执行文件所在目录:
现在,将你建立的Manifest文件拷贝到可执行文件所在的目录。
移动Manifest文件
1.打开资源管理器,到Visual Studio解决方案所在的目录。在这个目录里,你应该看到刚才建立的Manifest文件。(命名为<Executable Name>.exe.manifest)
2.选择该文件,并拷贝它。
3.将当前目录转到obj->Debug,在这个目录下,你可以看到可执行文件。
4.将拷贝的Manifest文件粘贴到该目录下。
添加Manifest文件到可执行文件:
接下来,在Visual Studio中打开可执行文件,并将Manifest文件作为一项资源添加到其中。
以资源方式添加Manifest文件
1.在Visual Studio中,在“文件”菜单下选择:打开->文件。
2.转到可执行文件所在的目录,并双击打开可执行文件。
3.在设计框中右击可执行文件名,选择“添加资源”。
4.在添加资源对话框中选择“导入”。
5.转到刚建立的Manifest文件所在的目录。(和可执行文件在同一个目录下)
6.双击打开Manifest文件,同时自定义资源类型对话框打开。
7.在资源类型对话框中,键入RT_MANIFEST即可。
8.在属性窗口,将ID设置为1。
9.最后,就是全部保存。
尝试新成果:
好了,到处为止,新的程序可以运行了。当你的运用程序运行在Windows XP下时,窗体上的控件将显示出富有未来派气息的Windows XP视觉风格。同时,你也可以试试按下按钮,看看新型的进度条的样子。
然而,当你的程序运行在其他的操作系统下时,那个Manifest文件应该被忽略,因为在其他操作系统上是没有版本为6.0的Comctl32.dll文件的。所以,窗体上的控件又会和以前的一样了。
接下来的步骤:
对于以上问题,我有更好的、更健壮的解决方案。那就是先进行操作系统的版本检查,然后根据检查结果,动态调用可执行文件下的Manifest文件从而动态的将FlatStyle属性设置为System。
以下给出了逻辑上的代码:
‘ Visual Basic
Private Sub RecursivelyFormatForWinXP(control As Control)
Dim x As Integer
For x = 0 To control.Controls.Count – 1
‘ 如果控件是从ButtonBase继承过来的
‘ 将FlatStyle属性设置为System
If control.Controls(x).GetType().BaseType Is _
GetType(ButtonBase) Then
CType(control.Controls(x), ButtonBase).FlatStyle = _
FlatStyle.System
End If
‘ 如果控件中包含了其他控件,那么进行同样的操作
If control.Controls.Count > 0 Then
RecursivelyFormatForWinXP(control.Controls(x))
End If
Next x
End Sub
// C#
private void RecursivelyFormatForWinXP(Control control)
{
for(int x = 0; x < control.Controls.Count; x++)
{
// 如果控件是从ButtonBase继承过来的
// 将FlatStyle属性设置为System
if(control.Controls[x].GetType().BaseType == typeof(ButtonBase))
{
((ButtonBase)control.Controls[x]).FlatStyle = FlatStyle.System;
}
// 如果控件中包含了其他控件,那么进行同样的操作
if(control.Controls.Count > 0)
{
RecursivelyFormatForWinXP(control.Controls[x]);
}
}
}
另外,你需要修改Load事件处理函数,从而判断程序是否运行在Windows XP下,Manifest文件是否要被用到:
‘ Visual Basic
Private Sub Form1_Load(sender As Object, e As System.EventArgs)
‘ 确定是在Windows XP下运行并且Manifest文件存在
If Environment.OSVersion.Version.Major > 4 And _
Environment.OSVersion.Version.Minor > 0 And _
File.Exists((Application.ExecutablePath + ".manifest")) Then
‘ 遍历各个控件
Dim x As Integer
For x = 0 To (Me.Controls.Count) – 1
‘ 如果控件是从ButtonBase继承过来的
‘ 将FlatStyle属性设置为System
If Me.Controls(x).GetType().BaseType = _
GetType(ButtonBase) Then
CType(Me.Controls(x), ButtonBase).FlatStyle = _
FlatStyle.System
End If
RecursivelyFormatForWinXP(Me.Controls(x))
Next x
End If
End Sub
// C#
private void Form1_Load(object sender, System.EventArgs e)
{
// 确定是在Windows XP下运行并且Manifest文件存在
if(Environment.OSVersion.Version.Major > 4
& Environment.OSVersion.Version.Minor > 0
& File.Exists(Application.ExecutablePath + ".manifest"))
{
// 遍历各个控件
for(int x = 0; x < this.Controls.Count; x++)
{
// 如果控件是从ButtonBase继承过来的
// 将FlatStyle属性设置为System
if(this.Controls[x].GetType().BaseType == typeof(ButtonBase))
{
((ButtonBase)this.Controls[x]).FlatStyle = FlatStyle.System;
}
RecursivelyFormatForWinXP(this.Controls[x]);
}
}
}
六.文章总结
在本文中,我给大家展示了以下几方面的内容:
1.具有Windows XP视觉风格的控件和Visual Studio中的标准控件的外观是截然不同的。
2.你可以将大部分的控件的外观设计成和Windows XP中的一样。
3.对于大多数的控件,这个设计过程只需要将它们捆绑到一定版本(版本6.0)的Comctl32.dll即可;对于其他的,你需要将FlatStyle属性设置为System;最后,还有一部分根本不用修改。
4.你可以编写检查操作系统版本的代码,从而确定Manifest文件是否需要用来动态设置各个控件的相应属性。