2016年7月29日 星期五

Windows程式畫面布局設計概念

這個布局部分有點多,所以可能的話會分幾次來講

基本上您會發現,相同應用的軟件,基本上畫面布局(Layout)都很像,佈局是指什麼,就是您在畫面上放置一些容器,讓框架內可以放置一些您的按鈕或者是輸入欄位,當然也可以是顯示圖片

大概長這樣
這是一個空白的Form

 如下,我加了一個Menu選單,一個狀態列,還有兩個Panel容器,其中Panel上面可以根據我的需要,放一些元件



















其中用Panel或者是其他容器(例如ScrollBox,Notebook),定出畫面的擺放,這叫做布局(Layout)

這裡就直接稱乎布局了

我們常常看到很多舊型的程式,都會有一些相似的布局
我們以C++ Builder為例(這是新版的)















最上方會有一個Menu,再來下面是一些快捷按鈕,左邊是屬性設計,右邊也是顯示一些資訊的地方,下方會有一個狀態列(其中他將狀態列換成自製的)
基本上C++Builder他的視窗可以到處搬動,這已經是屬於進階部份了,不過基本樣式是固定的

因此就我剛剛提到的那些基本元素,我大概拉一個基本樣式



















上方有一個Menu,下面是一些快捷按鈕,例如按完可以做計算還是幹嘛,總之就是一些按鈕,再來是最下方的狀態列,而中間兩個則是自己設計的框架,我的框架式左右兩個Panel,右邊是放參數的,左邊是主要顯示Client視窗,

基本上您會在很多古早的應用程式,大約Windows XP那個年代,大概都長這個樣子

這是最最最基本的,也就是您完成一個應用程式,假設小到不行,您可以參考這總方式做基本畫面布局,

總之就是很古老的做法就是了,基本上您看到現在的一些應用程式很花俏,充滿了無數的設計,所以這簡單的畫面布局基本上也不算什麼秘密了,要做到像一些大型軟件那樣,其實是很多設計的,絕對不是就這樣弄一個Form,然後全部東西都弄在這裡,就結束了

大型軟件是指什麼,例如您打開近代的WordC++ BuilderVisual Studio,他們會有一些框架,不同框架放不同的功能,其實您數一數可能也10~20種,您要在一個FHD的螢幕上,規劃10~20種框架,是不可能釘一釘Panel就處理完的,

因此這是一門學問也是一門藝術,但是基本上像這種大型軟件,框架可以到處釘,基本上這是模組化設計了(老實說我也不會),這就不再我們討論範圍,這一般沒有10來個人同時寫一支程式不太可能完成,我這裡會介紹的比較偏向於一個人能獨自完成的應用程式

如上面您看到我弄的框架,基本上很死,框架都釘好了,基本上是無法做任何變化的,

我們這裡談到的設計,牽涉到UX(User experience design ),也就是使用者體驗,
說他簡單,也很簡單,其實一個重要意義就是您的畫面如何設計,可以讓使用者操作起來最舒服,最方便
但是說他難,也很難,我們一支程式通常是從無到有,一般您在公司裡接案子,客戶不會一次就將所有要做的功能都跟您說,因此您的介面會一直增加新的東西

您會發現做一做,需求一直進來,您就加入很多東西,最後都塞到同一個地方,例如剛剛的圖,只有6個欄位,那要您的欄位一直加一直加,加到了20多個,通常來說,欄位太多要設定,如果您沒做分類處理,或者做一些分業,使用者會用起來很不舒服

假設您一支程式有一個功能,有18個參數可以控制,您以一個程式設計師來說最簡單的就是將欄位照順序分在一起


















這裡就不做命名了,總之就是不同參數的欄位,這一般有接過案子才能體會,我也不知如何表達,總之您就把他想成您有18個欄位可以控制就是了

基本上您沒做分類,要懂怎麼操作您的軟體看了就不想學了,
所以我們先做基本分類,好啦,就假設我這個程式可以用來設定動物身上的顏色,
假設相關的顏色欄位,狗有6個,貓有4個,鳥有8個,
我們做一個索引如圖


















這樣做簡單歸類,操作員就知道假設他要設定狗相關顏色參數,就改納六個欄位就好了,依此類推,我們程式設計目的做一支好用的程式給使用者,而不是只是為了完成功能賺錢而添加新的功能,這在一些比較落後的公司都不注重這個,往往就要啥參數就加啥,然後放一大堆,基本上要是我是使用者,我看到就不想用了,更別說對您的軟體有興趣

當然講這樣好像太淺了,很像我是白癡一樣,所以我就再衍伸出去

通常我們程式是可以縮放的,要是您把參數欄位放在Panel上面,您會發現縮小時,欄位被遮住了,如圖




















雖然看起來很弱智,但是假設是初學者對元件不熟,一定做到後來就會發生這個錯誤,基本上我們就使用ScrollBox,他會有ScrollBar可以拖曳,就不會擋到您的欄位了














這樣其實還不夠,通常使用者再看欄位時,可能一次10個以上就會抱怨了,所以除了分類,我們還要做分頁,最簡單的Solution就是PageControl,當然有很多替代的,也可以自己設計,這裡就不講自己設計的部分(因為要寫Code),其實自己設計的話會比較漂亮,所以我們將PageControl做三頁,將這些欄位分別放入PageControlTabSheet裡面


















您只要切換頁籤就可以設定不同的動物顏色,但是您會發現問題又來了,畫面縮小時又會欄位了















基本上就是
PageControl本身不會有ScrollBar,因此我們需要在它上面分別放上ScrollBox,再在ScrollBox上面放元件,這樣當縮小時就又會出現ScrollBar















基本上您會發現ScrollBox很好用,他會幫您自動產生ScrollBar但是必須是元件跑到ScrollBox外面才會出現,

原則上如果您沒做畫面設計的話您大多時間會做我上面在做的事情,就是畫面布局,及參數分類管理,原則上可用的元件還是要熟拉,假設您沒有要深入,會到這裡就可以了

偷偷給您看經過畫面設計會有啥不一樣,我大概弄給您看,大概就是將PageControl改成自己用程式碼設定呼叫頁面顯示,往後教程我會弄一個範例給您下載研究,您會發現其實軟體是有很多技巧的,而不是就只是拉拉元件,兜兜功能,寫寫核心演算法就沒了

這裡只是先秀給您看一張圖
















自製的PageControl,其中滑鼠放上去按鈕(其實他是Panel做的)他會變色,
這技術其實可以常常在網頁上看到,是一個模組化技巧(由於關鍵,就晚點再秀了(0 .0明年看有沒有機會),只要您會模組化,這種東西可以千變萬化)


先到這了

開發的函式物件導向化1

講到物件導向,其實就是把可以reuse的東西包起來,使他可以reuse,包起來不只是只能寫成一個function,您只要是一段程式碼可以重新呼叫,不管您怎樣包都可以,例如您可以裝在class裡或者是namespace裡也可以

我們舉我們在學校第一個學的演算法,也就是bubble sort(氣泡排序法)

當我們要處理一串數字依序排列,就要使用到他,下面會附一個我測試的程式碼壓縮檔在裡面,可以直接使用C++ Builder

首先我們先拉幾個欄位,使他可以輸入數字,這裡是示範,只使用五個













也就是左邊五個欄位輸入數字後,按下button,會依序排列在右邊

首先我們先定義接口,也就是將處理的程式碼拆成三段

/////////////////////////
將欄位存入陣列
處理片段
將處理結果存回欄位
/////////////////////////

這邊假設看的人都知道C++ Builder宣告一個陣列,或配置一塊記憶體,是不會初始化內容的,也就是裡面的值不會都是0,其實講細節要弄成影片比較好懂,儘管影片錄的不好,但是可以做一連貫的說明,其實是比較好的,這邊沒有弄成影片,所以就講得比較簡化一點

#define n 5
void __fastcall TForm2::BitBtn1Click(TObject *Sender)
{
    int src[n];
    // 對陣列初始化
    for(unsigned int i = 0;i < n; ++i)
    {
        src[i] = 0;
    }

    // 也可用memset處理
    //memset(src, 0, n * sizeof(int));

    // 讀入值
    src[0] = atoi(AnsiString(Edit1->Text).c_str() );
    src[1] = atoi(AnsiString(Edit2->Text).c_str() );
    src[2] = atoi(AnsiString(Edit3->Text).c_str() );
    src[3] = atoi(AnsiString(Edit4->Text).c_str() );
    src[4] = atoi(AnsiString(Edit5->Text).c_str() );
    
    ///////////////////////////
    // 由小排到大
    for(unsigned int j = 0; j < n; ++j)
    {
        for(unsigned int i = j + 1; i < n; ++i)
        {
             if(src[j] > src[i])
            {
                 // 交換
                 int temp = src[j];
                 src[j] = src[i];
                 src[i] = temp;
            }
        }
    }
    ///////////////////////////
    Edit6->Text = src[0];
    Edit7->Text = src[1];
    Edit8->Text = src[2];
    Edit9->Text = src[3];
    Edit10->Text = src[4];
}

如這樣排列,程式碼會比較好懂,我們必須分段處理並且加入適當的註解,這樣才比較清楚,也方便後面要做的動作

這樣是由小排到大,這我已經確認過了

也就是

/////////////////////////
將值讀入陣列
處理
存回欄位
/////////////////////////

這樣基本上就達到排序的功能了

再來我們演算法寫好後,通常為了往後方便(就是以後可以reuse),所以要將它物件化,一般用function就可以了

#define n 5
void bubble_sort(int* src)
{
    for(unsigned int j = 0; j < n; ++j)
    {
        for(unsigned int i = j + 1; i < n; ++i)
        {
            if(src[j] > src[i])
            {
                // 交換
                int temp = src[j];
                src[j] = src[i];
                src[i] = temp;
            }
        }
    }
}

void __fastcall TForm2::BitBtn1Click(TObject *Sender)
{
    int src[n];
    
    // 對陣列初始化
    for(unsigned int i = 0;i < n; ++i)
    {
        src[i] = 0;
    }
    
    // 也可用memset處理
    //memset(src, 0, n * sizeof(int));

    // 讀入值
    src[0] = atoi(AnsiString(Edit1->Text).c_str() );
    src[1] = atoi(AnsiString(Edit2->Text).c_str() );
    src[2] = atoi(AnsiString(Edit3->Text).c_str() );
    src[3] = atoi(AnsiString(Edit4->Text).c_str() );
    src[4] = atoi(AnsiString(Edit5->Text).c_str() );

    ///////////////////////////
    bubble_sort(src);
    ///////////////////////////

    Edit6->Text = src[0];
    Edit7->Text = src[1];
    Edit8->Text = src[2];
    Edit9->Text = src[3];
    Edit10->Text = src[4];
}

如上,您看這樣就變成一個可以reuse的函式,假設您要排五個數字,就可以用這個函式來處理,往後您只要將函式存在一個cpp,帶走就可以用了,就不用每次都在重寫,達到reuse的作用

那這個函式只能排五個,我們要是要排10個或20個呢,一個函式好不好用,在於它的彈性,也就是目前寫法他只能處理五個,再多就不能用了,這種東西就是太死,所以不是那麼好,因此做了一些修改

#define n 5
void bubble_sort(int* src, int m)
{
    for(unsigned int j = 0; j < m; ++j)
    {
        for(unsigned int i = j + 1; i < m; ++i)
        {
            if(src[j] > src[i])
            {
                // 交換
                int temp = src[j];
                src[j] = src[i];
                src[i] = temp;
            }
        }
    }
}

void __fastcall TForm2::BitBtn1Click(TObject *Sender)
{
    int src[n];

    // 對陣列初始化
    for(unsigned int i = 0;i < n; ++i)
    {
        src[i] = 0;
    }
    // 也可用memset處理
    //memset(src, 0, n * sizeof(int));

    // 讀入值
    src[0] = atoi(AnsiString(Edit1->Text).c_str() );
    src[1] = atoi(AnsiString(Edit2->Text).c_str() );
    src[2] = atoi(AnsiString(Edit3->Text).c_str() );
    src[3] = atoi(AnsiString(Edit4->Text).c_str() );
    src[4] = atoi(AnsiString(Edit5->Text).c_str() );
    
    ///////////////////////////
    /*
    // 由小排到大
    for(unsigned int j = 0; j < n; ++j)
    {
        for(unsigned int i = j + 1; i < n; ++i)
        {
             if(src[j] > src[i])
             {
                  // 交換
                  int temp = src[j];
                  src[j] = src[i];
                  src[i] = temp;
             }
        }
    }
    */
    //bubble_sort(src);
    bubble_sort(src, n);
    ///////////////////////////

    Edit6->Text = src[0];
    Edit7->Text = src[1];
    Edit8->Text = src[2];
    Edit9->Text = src[3];
    Edit10->Text = src[4];
}

N已經用掉了,所以我換成m,那個函式就可根據定義的n處理不同數量變數
其實這樣彈性不夠,現在是由小排到大,那要是我要由大排到小呢,我們再做這樣的修改

void bubble_sort(int* src, int m, bool direct) // 0是由小排到大 1是由大排到小
{
    for(unsigned int j = 0; j < m; ++j)
    {
        for(unsigned int i = j + 1; i < m; ++i)
        {
             if( (src[j] > src[i] && !direct) || (src[j] < src[i] && direct) )
             {
                 // 交換
                 int temp = src[j];
                 src[j] = src[i];
                 src[i] = temp;
              }
         }
    }
}

void __fastcall TForm2::BitBtn1Click(TObject *Sender)
{
    int src[n];
    // 對陣列初始化
    for(unsigned int i = 0;i < n; ++i)
    {
        src[i] = 0;
    }

    // 也可用memset處理
    //memset(src, 0, n * sizeof(int));

    // 讀入值
    src[0] = atoi(AnsiString(Edit1->Text).c_str() );
    src[1] = atoi(AnsiString(Edit2->Text).c_str() );
    src[2] = atoi(AnsiString(Edit3->Text).c_str() );
    src[3] = atoi(AnsiString(Edit4->Text).c_str() );
    src[4] = atoi(AnsiString(Edit5->Text).c_str() );

    ///////////////////////////
    /*
    // 由小排到大
    for(unsigned int j = 0; j < n; ++j)
    {
        for(unsigned int i = j + 1; i < n; ++i)
        {
             if(src[j] > src[i])
             {
                 // 交換
                 int temp = src[j];
                 src[j] = src[i];
                 src[i] = temp;
              }
        }
    }
    */

    //bubble_sort(src);
    //bubble_sort(src, n);
    bubble_sort(src, n, 0);
    ///////////////////////////

    Edit6->Text = src[0];
    Edit7->Text = src[1];
    Edit8->Text = src[2];
    Edit9->Text = src[3];
    Edit10->Text = src[4];
}

這樣function可以做兩種方向的處理了,往後就會非常方便了

下面我們要做一些轉換,將它換成vector,因為我這邊要改成動態無限擴增,並簡化函式,目前需要三個欄位,如果換成vector,那基本上我們只需要兩個欄位,因為vector可以查容器的大小(size())

由於我程式碼要存成範例,因此我再另外拉一組介面,重新弄一遍






















void bubble_sort(vector<int>& src, bool direct) // 0是由小排到大 1是由大排到小
{
    int m = src.size();
    for(unsigned int j = 0; j < m; ++j)
    {
         for(unsigned int i = j + 1; i < m; ++i)
        {
             if( (src[j] > src[i] && !direct) || (src[j] < src[i] && direct) )
             {
                 // 交換
                 int temp = src[j];
                 src[j] = src[i];
                 src[i] = temp;
             }
        }
    }
}

void __fastcall TForm2::BitBtn2Click(TObject *Sender)
{
    vector<int> src;

    // 讀入值
    src.push_back( atoi(AnsiString(Edit11->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit12->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit13->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit14->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit15->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit16->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit17->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit18->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit19->Text).c_str() ) );
    src.push_back( atoi(AnsiString(Edit20->Text).c_str() ) );

    ///////////////////////////
    bubble_sort(src, 0);
    ///////////////////////////

    Edit21->Text = src[0];
    Edit22->Text = src[1];
    Edit23->Text = src[2];
    Edit24->Text = src[3];
    Edit25->Text = src[4];
    Edit26->Text = src[5];
    Edit27->Text = src[6];
    Edit28->Text = src[7];
    Edit29->Text = src[8];
    Edit30->Text = src[9];
}

您看這樣就可以處理10筆了,這邊有個小技巧,可以用列舉的方式,來處理,往後您只要對陣列新增欄位名稱,就可以做處理了

為了程式碼簡潔,我們再拉個button,總之後面我會附一個專案檔,打開就可以研究了

void __fastcall TForm2::BitBtn3Click(TObject *Sender)
{
    vector<int> src;
    struct Object_Mapping
    {
        TEdit* src;
        TEdit* dst;
    };

    Object_Mapping om[] =
    {
        {Edit11, Edit21},
        {Edit12, Edit22},
        {Edit13, Edit23},
        {Edit14, Edit24},
        {Edit15, Edit25},
        {Edit16, Edit26},
        {Edit17, Edit27},
        {Edit18, Edit28},
        {Edit19, Edit29},
        {Edit20, Edit30},
    };

    int k = sizeof(om) / sizeof(Object_Mapping);
    src.clear();

    // 讀入值
    for(int i = 0; i < k; ++i)
    {
        int value = atoi( AnsiString(om[i].src->Text).c_str() );
        src.push_back( value );
    }

    ///////////////////////////
    bubble_sort(src, 0);
    ///////////////////////////

    for(int i = 0; i < k; ++i)
    {
        om[i].dst->Text = src[i];
    }
}

這邊用到一個列舉的小技巧,您可照綠色文字的方式建立一個對應,
也就是來源對應輸出,我們得到物件的指標後,可以用
int k = sizeof(om) / sizeof(Object_Mapping);

取得他的size,然後我們就可以用for迴圈處理了,基本上要再加一組欄位,
只要將對應的指標如下處理,非常的方便

struct Object_Mapping
{
    TEdit* src;
    TEdit* dst;
};

Object_Mapping om[] =
{
{Edit11, Edit21},
{Edit12, Edit22},
{Edit13, Edit23},
{Edit14, Edit24},
{Edit15, Edit25},
{Edit16, Edit26},
{Edit17, Edit27},
{Edit18, Edit28},
{Edit19, Edit29},
{Edit20, Edit30},
{Edit31, Edit32},
};

這是用C++的好處,我不確定C#是否有這種東西可以使用(因為C#沒有指標),或許有,或許沒有,

而且C#是沒有vector的,不太方便就是了

上面程式碼有點效率問題,就是vector在pushback時,要是超出他已經配置的大小,會重新配置記憶體,並搬移到新的記憶體位置(內部處理動作)

http://pingyeh.blogspot.tw/2011/09/stl-vector.html

至於我們怎麼知道他有做這個動作,其實很簡單,我們知道配置記憶體要時間,
所以我們測量您程式片段所需的時間就可以看出,這以後我再示範,總之記得
Vector如果預先就知道您要push_back的大小,要事先reserve,如下

void __fastcall TForm2::BitBtn3Click(TObject *Sender)
{
    vector<int> src;

    struct Object_Mapping
    {
        TEdit* src;
        TEdit* dst;
    };

    Object_Mapping om[] =
    {
        {Edit11, Edit21},
        {Edit12, Edit22},
        {Edit13, Edit23},
        {Edit14, Edit24},
        {Edit15, Edit25},
        {Edit16, Edit26},
        {Edit17, Edit27},
        {Edit18, Edit28},
        {Edit19, Edit29},
        {Edit20, Edit30},
        //{Edit31, Edit32},
    };

    int k = sizeof(om) / sizeof(Object_Mapping);

    // 將整個陣列丟掉
    vector<int>().swap(src);

    // 預配置陣列大小
    src.reserve(k);
    // 由於每個值都會蓋過去,所以我們不對vector初始化
    // 讀入值

    for(int i = 0; i < k; ++i)
    {
        int value = atoi( AnsiString(om[i].src->Text).c_str() );
        src.push_back( value );
    }

    ///////////////////////////
    bubble_sort(src, 0);
    ///////////////////////////
    for(int i = 0; i < k; ++i)
    {
        om[i].dst->Text = src[i];
    }
}

其實很多技巧都牽涉到底層的硬體運作模式,所以寫程式不能只會軟體知識,到後期要高效能要了解一些硬體的東西,或是底層的處理方式,

但是基本上大型的程式有時候為了維護方便,基本上有時候會使用偷懶的方式,就是假設要設定一些指令,假設設定處理時間不會太久,會將設定指令都寫在一起,也就是我要設定指令時,呼叫相同一個函式就可以了,不用呼叫不同函式

最後,其實上訴這些東西算是一個概念,而要做排序在C++是不會使用bubble sort,會用STL的sort

Bubble sort要花O(n)
Sort指令只要花(nlogn)

基本上Sort會比較快就是了,當排序的東西比較多的時候,怎麼知道呢,就是量一量程式片段的時間就知道了,這裡一樣也不做示範

Sort用法如下

#include <algorithm>

void __fastcall TForm2::BitBtn3Click(TObject *Sender)
{
    vector<int> src;

    struct Object_Mapping
    {
        TEdit* src;
        TEdit* dst;
    };

    Object_Mapping om[] =
    {
        {Edit11, Edit21},
        {Edit12, Edit22},
        {Edit13, Edit23},
        {Edit14, Edit24},
        {Edit15, Edit25},
        {Edit16, Edit26},
        {Edit17, Edit27},
        {Edit18, Edit28},
        {Edit19, Edit29},
        {Edit20, Edit30},
        //{Edit31, Edit32},
    };

    int k = sizeof(om) / sizeof(Object_Mapping);

    // 將整個陣列丟掉
    vector<int>().swap(src);
    // 預配置陣列大小
    src.reserve(k);
    // 由於每個值都會蓋過去,所以我們不對vector初始化
    // 讀入值
    for(int i = 0; i < k; ++i)
    {
        int value = atoi( AnsiString(om[i].src->Text).c_str() );
        src.push_back( value );
    }
    
    ///////////////////////////
    //bubble_sort(src, 0);
    sort(src.begin(), src.end() );
    ///////////////////////////

    for(int i = 0; i < k; ++i)
    {
        om[i].dst->Text = src[i];
    }
}

:D簡單又方便,很多東西其實在C++都已經有現成的了,所以不用自己再寫,而且那些效率都是很高的,其實我也只會到這邊,還有一些特別的我也不是很清楚就是了

短短的一個bubble sort,可以牽扯到那麼多,這些都是基本的,不會真的不太容易寫好程式

而C#就我淺淺的知道,他是比較簡單,但似乎他很多東西都沒有(例如好用的vector和STL),所以到底好不好用就只能問會用的人了

這裡是範例連結
https://drive.google.com/open?id=0B6u5oifJv3EKYmxLY05TcjVJbkU



當程式設計師該有的覺悟

程式設計是很邪門的一門藝術,說他難,其實超級難,說他簡單,其實如果沒寫到Assembly Language,您會發現一個好的演算法,其實Code很少,並且易讀的,可能兩百行就達到您要的功能,並且超快,兩百行會很難打嗎,其實有寫好得Code就複製貼上就可以用了(笑)

當然如果是需要很多演算法的處理,也是數千行的程式碼拉,

就光學檢測這一塊,其實要找瑕疵就是二值化或Sobel然後Blob,找刮痕不外乎就是利用中值濾波找出背景,然後前景背景相減,剩下就是瑕疵了,上述演算法其實一般進去公司,公司都會有現成寫好的,就算沒有,其實有IPP(Intel的影像處理函式庫),您只要會呼叫他就可以了,而且速度超快,要寫的演算法通用的都有人幫您處理好了,不太需要自己寫過,

去哪間公司,其實到最後都一樣的,這邊提到IPP很快,那假設自己寫了5~6年某演算法處理速度已經到極限了,您知道有IPP這種東西比較快,您如果會使用的話,不會去用他嗎,怎麼可能?,最後就變成大家都用一樣的東西,每間公司有獨門的技術嗎?都是呼叫IPP,技術的不是技術了,

當然這裡只是指常用的演算法,會有一些函式庫可以呼叫,一些特殊的處理還是要自己寫的,不過就像提到的,其實很多東西都人家幫您寫好了,您只要拿的到並且會用,都不用自己寫的,所以說實在的也等於處理一下UI的東西,就可以賣了

其實就是因為很多東西被物件導向化(就被打包成一個函式),省下工程師的很多力氣,但是變成門檻太低了,因為大家都會做,導致薪水下降,要追求高薪必須越做越多,這裡是說有拿到一些現成的東西的,

那要是沒有一些現成函式呢,例如您學校剛畢業,什麼都沒有,那人家有現成函式的產品一下就做完了,您全部自己寫就會變成,累得要死,難易度超高,所以說寫程式也可以說非常難

看到這邊,通常我們都是屬於沒有自己東西的人,所以要自行創業的話,什麼東西都沒有,簡直是天方夜譚,不可能的任務

所以我們會遇到的狀況變成沒有資源,要花比人家多好幾倍的力氣,達到跟人家一樣的水平

總之就跳針一下,就是會有很多問題及工作量拉,所以要有以下覺悟

1. 寫Code通常是一翻兩瞪眼,通常一個地方出錯,整支程式就會一直當機,導致不能用,所以一定會遇到一直查問題的狀況,通常查問題會花很多時間,查不到或不會解決,就會一直卡在那裏,這種狀況是正常上下班不可能解決的,有時候沒查到Bug就只能沒日沒夜的查,所以如果您想做好,一定要有加班的心理準備

2. 如前面所提,我們是屬於沒有資源的那一群,所以如果真的要做程式設計師,最好學得差不多時,要開始累積自己的東西,例如寫一個函式要1個月,假設您是人家的員工,通常有簽一些條款,就是做出來的東西是公司的,這樣就變成,你做出來的程式您換公司不能拿來用,也就是就算您有能力再寫一份出來,您也要浪費一個月,

因此當覺得自己能力夠時,最好要開始累積自己的東西,一個函式也是東西,往往薪水和自己的東西比較起來,自己的東西比較重要,因為如果您想永續的發展,絕對要累積一些自己的東西,並在進入下一家公司時,做一些契約上的協調,也就是東西我在這家公司用完了,不管換公司還是自己創業,這些東西我必須可以直接拿來用,不然您會陷入一直打掉重練的狀況

3. 寫Code是很累的一件事,絕對不要想要偷懶,他有一套標準做法,例如class複製一定要寫Copy Constructor你如果偷懶沒寫,他在一些狀況下會出錯,這個以後我再做描述,總之不能為了省力,採取偷雞的作法,這樣您的東西會有問題,

4. 一些AOOP的東西是有用的,其實您只是沒看過人家怎麼用,它可以幫您省下很多Code,所以不要只停留在C語言的境界,您寫久了會操死,又寫不好

5. 一定要養成定期註解,通常趕案子不會一直註解,但是一段時間一定要註解一下,不然很容易忘記,也是不能為了省打字,就都不註解

6. 通常一個東西要賣錢,一定會有相關應用,像AOI程式就牽涉到做設備,通常要做軟體整合,工作量是很大的,而且往往您多想5分鐘做一些預先規劃,就像是模組預先規劃可以擴充,往後您修改時會比較好改,要是寫得太死,往後您要改等於是打掉重寫,一樣您會累死


總之看到這裡,結論就是不要懶,多想兩分鐘做規劃,往往比人家做事慢一點,但是往後您會省下很多力氣,真得要多想多練習,主管常常會亂追進度,寫Code不是賣東西,賣越快做越好,所以要按照自己步調來寫,不然通常往往會寫不好的(就是照主管的想法做事),並且不要太怕加班,因為有些東西每天只做8小時是做不好得,何況一般人上班會偷懶,可能一天扣掉開會,打雜,不知道花在寫Code上面有沒有4小時(這是真的喔,通常能持續寫6小時算很認真了)

2016年4月10日 星期日

Windows程式基本框架(以AOI為例)

基本上一個程式會Focus在一個主題上,以Word來說,他是文書處理軟體,那它主要目的就是輸入文字及排版,做出來的可以對文字及圖片編輯及排版,而基本上同個領域都有相似的設計方式,可以看到每個文書軟體,哪個不是一張白布,一個游標,可以輸入文字的,

當然會有一些不同設計,根據每個人的不同想法,但是基於使用上,Focus的設計會根據要設計的軟體,經由時間的累積,逐漸產生很固定的程式撰寫方法,講個廢話,有人有看過網頁軟體有提供不一樣的瀏覽畫面嗎,頂多框架不同,操作不同,但是主要功能是瀏覽網頁,他瀏覽網頁那塊的設計基本上是固定的,一家網頁提出個頁籤功能,其他廠商過一陣子覺得這個功能實用,以寫得出來的前提下,過兩天就生出類似的頁籤管理了

更實際的例子,古早算1加到n可能是用for迴圈加的,但是當有人發明了梯形公式,這個東西足夠好,當大家都會時,未來算1加到n,基本上都是用梯形公式

一個領域的軟體也是經由很多工程師一直演化發展出來的,其實我們就一個初入該領域的人,是不可能什麼東西都自己想的,原則上是靠臨摹,講難聽一點就是研究完後抄襲

所以呢,我們原則上還是必須要到一些有技術性底子的公司,在經由維護他的案子,順便學習他的案子的所有一切,了解他的優缺點後,才是我們提出改進,並做出屬於我們自己的東西

總之就是不可能靠自己憑空想,儘管花了10多年自我研究,就生出很有價值的東西,

所以呢,還是要想辦法得到門票去一些有技術底子的公司取得有用的謀生能力,

後面要講的是AOI他的程式基本框架,當然也不可能是我發明的,我也是近公司才開始學習的,

進入正題

AOI就是所謂的光學檢測,他其實很雜,牽涉到機構,光學,電子,軟體,那一個人怎麼會所有領域,其實去上班就知道了,寫軟體的就知道這四個領域相關常識,其實要做時,軟體外的就發包出去請機構商做,光源買現成的…之類的,我們還是主要Force在軟體,

AOI做出來的東西,原則上要搭配自動化設備,做到一台機器並且有檢出功能才算是完整

基本上牽涉到客戶檢測的產品,產品基本上是多樣性的,假設檢測一個主機板,每個公司的主機板長的都不一樣,甚至同一家公司會有多款的主機板,那我們要賣給一個客戶,我們的軟體就必須可以編輯檢測功能,

當然有些小型檢測軟體,他只是Force在一個檢測的項目,不過那種案子對AOI來講,比較接近練功用

我們工作不外乎就是賺錢學知識,所以小型檢測軟體這裡只是稍微帶過,反正小型的檢測軟體就是一個自動化流程,我們可以做基本編輯,並且提供一個主題的檢測功能,只要能達到檢測功能,並且讓他用自動化連續跑,就設計完了,不太需要太複雜或太難的設計

但是當作到中大型的,寫軟體非常像蓋一個工廠,

假設要控制打光的光源開關,我們就要做一個光源模組控制光源,
假設要取像,就必須寫一個取像模組來得到影像
假設要控制IO,當然就是要能控制IO存取的模組,

總之一間公司是很多部門的結合,那大型軟體,就是很多模組的集合,

想想一間公司的人資部門,假設有固定一種管理模式,把她想成是一個模組,那要是公司想要用不同方式管理人資,就把這個人資部門fire掉,在另外找一批人成立一個人資部門,也就是換模組拉,以軟體為例,電源控制器很多款,那我一套大型軟體,就會做一個模組接口,這個接口可以掛上控制不同電源控制器的模組,想切換只需要用ComboBox做一下選擇,就可以控制另一款電源控制器

這裡不講實現方法,不過講實際一點就是使用AOOP的Virtual來重載模組掛載,所以學校教的一些進階寫程式知識,其實不完全是那麼理論,那麼的傳統知識,其實是我們不會應用爾已,但是說實在的學起來不知道怎麼用,學的時候真的很無趣

要編這些故事其實挺麻煩的,因此我直接列舉AOI程式框架的模組:
1. 取像模組 – 沒影像要怎麼檢測
2. 參數管理模組 – 例如我要將檢驗結果儲存在檔案裡,我們不可能把路徑寫死,因此要提供參數輸入給使用者設定
3. 檢測功能的基本模組 – 用來寫檢測演算法的
4. 畫面編輯功能模組 – 例如我要檢什麼位置,就在該位置拉一個框,到時候我們就對那個框的位置採取其中一種檢測功能
5. IO模組 – 我們要判斷一個檢測產品是不是良好,必須送信號給自動化機械,請他們幫我們做動作
6. 光源模組 – 我們打光有時會調不同亮度(電壓不同拉),需要有一個模組來控制電源控制器
7. Motion模組 – 假設要掃Line Scan影像,必須控制載台移動取像,就必須完成這一個模組
8. Model模組 – 由於程式必須用編輯的方式處理,那編輯完的資訊就必須儲存起來,以便在下次打開程式時,載入相同的檢測設定,以便可以直接進行檢測
9. 噴墨模組 – 就是送字串給噴墨設備,請他們幫我們處理噴墨動作


以上是一些比較大的模組

就實際實現軟體細節的,下面列出一些細節模組:
1. 執行緒池 – 管理執行緒的,因為現在電腦有多核心,我們將不同的檢測,批次丟給多核去處理速度會比較快
2. 參數處理模組 – 例如我要存檔,必須做一個參數存檔模組,可以讓我們利用比較簡易的寫程式方式,就將參數寫入檔案或者讀出檔案
3. 背景處理用的Cmd執行緒 – 就是一個Block住的執行緒,我們下指令時他才會進行處理動作,用以將處理丟到背景去做,例如我要存圖,就指派一個執行緒去存圖,我們視窗程式就不會畫面卡住
4. Form欄位及參數對應模組 – 有了該項設計,欄位只要呼叫一個函式,就可將所有Form欄位存入結構或者是顯示在欄位上
5. 檢測結果模組 – 我們檢測完整個產品,每個地方有沒有問題必須存起來,以便到時候查看時可以將它畫到畫面上讓人家知道是瑕疵位置
6. 拉框模組 - 就是要檢測位置要用一個框表示,檢測時對該框的相對應位置進行檢測,這個框可以放大縮小或者是旋轉或增加節點或刪除(類似PhotoShop那樣)


總之講不完拉,全部提一遍其實沒太大作用就是了,一般要看過類似的案子,看上述內容才會知道我在說什麼,

想想我這裡隨便列就15項了,要一個人完成所有模組,真的不是簡單的事,更何況還要自己想出來,這是不可能的,所以還是建議必須要想辦法進去一些有底子的公司學習,接著在看我這篇文章,會比較有感覺的:D

這裡最後要提的是,每個模組又可以多個,例如光源控制器可以開多組光源控制器,相機可以取多張影像檢測,IO也可以多張IO卡,Motion控制可以控制多軸,Model也可以做繁衍,例如檢測相同的模型用一個Model,那要是兩類就必須有兩個不同教導的Model,總之什麼都可以繁衍,

但說實在的其實一般公司在做,也不是每個東西都做繁衍,可能都只是固定數目的模組,所以不需要想太複雜:總之還是必須循序漸進,不能太急,先看看小案子再來接手大案子,會對學習比較有幫助,因為案子大了其實不是那麼好研究的

Windows程式主體架構建置前的一些知識

基本上在C++ builder寫Windows程式之前,我們必須要了解windows作業系統,他其實是一個龐大的lib庫,他有非常多已經寫好的函式可以呼叫,您可以上msdn去查,例如CreateThread,您可以利用它來產生一個執行緒,在背景幫您跑工作,

當然效率就不講了(其實已經蠻快的),效率要好其實intel的IPP裡面也提供了intel自己開發的執行緒可以建立(似乎是說效率比較好)

這裡不提Intel的,總之,Windows有很多的DLL(就是Win32 DLL,其實64Bit也可以用啦,只要您作業系統是64bit的),裡面提供無數的函式可以呼叫,

在Visual C#裡面要使用這些函式,必須如下面方式

[DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int CreateThread(
IntPtr lpThreadAttributes,
UInt32 dwStackSize,
IntPtr lpStartAddress,
IntPtr param,
UInt32 dwCreationFlags,
UInt32 lpThreadId
);

然後您就可以使用這個函式,而在C++ Builder裡面,其實大部分的函式可以直接使用(例如CreateThread),就算不能使用只要去msdn查lib和.h是啥,include進來就可以呼叫了,除了這強大的Win32函式庫,像是STL等很多東西加個對應的.h檔就可以直接使用了,此外C++ Builder本身還封裝一些函式庫,例如您要判斷檔案存不存在,只要FileExists(檔名)就可以了,說實在的挺方便的,

其實像Windows有一些基本結構,例如RECT,BITMAP,HDC...等,他也有一些對應的Class繼承,例如TRect,您可以利用他做一些的處理,而不是只是一個結構,自己還要寫函式去處理細節

總之...很多東西東西都已經實現好了,我們可以直接使用來做處理,當然要是有些功能沒有現成的函式可以處理,那麼我們就必須將底層的Win32函式兜成一個可以用的模組,來呼叫使用,例如要用RS232送字串,我們就利用CreateFile(做法要上網查,網路都有),開啟Com Port,接收或送字串,要是要網路功能,其實他有Indy的套件可以使用,但是如果要自己用Win32函式庫兜功能呢,也是可以的,總之要上網研究一下,網路上有些程式片段只要再改一下,就可以直接使用了

總之就是說有很多函式啦,上萬個,但是其實我們做案子是有分領域的,例如做網路的,那你就只要會網路那塊的函式,做系統整合的,他有一些基本的系統整合相關的函式,原則上會您那個領域的就好,並不是說要將一萬多個函式通通摸過

很多東西上網查都有,但是要兜起來能動說實在的也不太容易,要Try很久,總之就是慢慢學習了

另外就是要會使用lib和DLL,像我做系統整合的(AOI光學檢測),往往會用到一些設備的函式,這時候通常廠商舊提供lib或DLL,並給您他的.h,這時候我們就必須用動態連結DLL的方式,來讓我們可以使用他們提供的函式,又或者是使用ImportLib轉換成C++ Builder能用的Lib來使用DLL,當然這裡就不講這個,總之先提個概念就是了

我們寫一個程式,除了呼叫函式外,就是要寫一個高效能的軟體,

最基本的要知道我們電腦一些元件的速度
CPU>L1 Cache>L2 Cache>L3 Cache>Memory>硬碟>隨身碟>外部設備,
這其實計概上有寫,實際您怎麼確定CPU是最快的呢,其實我們有軟體方法可以測,

例如我寫個For迴圈,裡面就是1+到n,這時候通常只在快取和CPU上跑,您用計算時間的函示去量他的時間(clock(),方法請上網找),

而我們要測記憶體,就是使用memcpy,您就做兩個大一點的陣列,利用memcpy在for迴圈裡面做,您會發現相同指令數,他會比較久,要是您在for迴圈做檔案存取,那又會更久了

下面是例子:
CPU&Cache
double k = 100000000;
double n = 0;
for(double i = 0;i < k; ++i)
{
++n;
}
long end = clock();
double time_ = (end - start) / CLOCKS_PER_SEC;
ShowMessage(time_);
這裡需要561ms

Memory存取
int a[1000];
int b[1000];
long start = clock();
double k = 1000000;
double n = 0;
for(double i = 0;i < k; ++i)
{
memcpy(a,b,1000*sizeof(int));
}
long end = clock();
double time_ = (end - start) / CLOCKS_PER_SEC;
ShowMessage(time_);
我for迴圈還比較少就要9.36秒

硬碟存取
FILE* file_ = fopen("c:\\a.txt","w");
long start = clock();
double k = 50000;
double n = 0;
for(double i = 0;i < k * 1000; ++i)
{
fprintf(file_, "%d", i);
}
long end = clock();
fclose(file_);
double time_ = (end - start) / CLOCKS_PER_SEC;
ShowMessage(time_);

由於跟記憶體複製比,所以複製長度1000要做1000次,所以我乘上1000
這要20秒

外部設備(就更久,其實我知道的就只有網路和RS232,總之比較久就是了)

當然硬碟還有SSD(固態硬碟)和一般硬碟,誰比較快,您就用windows複製大型檔案看看就知道了,就是從SSD複製到SSD,硬碟複製到硬碟,

總之,假設是一個指令,越是低端的設備會越慢,這個知識牽扯到我們寫程式效率的問題,假設在for迴圈裡面存取多次低端設備,會慢很多,通常假設沒必要,for迴圈內是不處理硬碟存取的,當然這種事情很少發生,

還有一種東西也是比較慢的,也就是GDI+指令

GDI+是圖形設備介面,他的指令丟在for迴圈裡面跑也很慢,不只是GDI+指令,VCL的屬性設定或讀取也是

例如:
long start = clock();
double k = 10000;
int n = 0;
for(int i = 0;i < k; ++i)
{
n += i % 1000;
//Height = i % 1000;
}
long end = clock();
double time_ = (end - start) / CLOCKS_PER_SEC;
ShowMessage(time_);
這只要0秒

long start = clock();
double k = 10000;
int n = 0;
for(int i = 0;i < k; ++i)
{
//n += i % 1000;
Height = i % 1000; // 設定Form的高,由於設超過螢幕解析度,他會pass調指令,所以這裡%1000,保證他可以下10000次設定VCL元件的指令
}
long end = clock();
double time_ = (end - start) / CLOCKS_PER_SEC;
ShowMessage(time_);
您會發現只設定Form的高10000次要3.3秒

我們必須知道一些基本的知識,不然是不能寫出高效能的程式的,這邊只是稍微提到,並不深入(其實我也沒全部了解),總之,通常當您的處理太慢時,就必須要做profile(時間分析),看把哪裡mark掉會比較快,再想辦法改寫,使的功能正常外,效能又比較高

當然有時候會為了節省程式碼,一次執行很多多餘的設定畫面VCL物件的指令,這又是另一個問題了,這關於寫Code的速度,以及管理上的方便,有時必須這麼做

但是在實現功能時,通常要注意上面舉的例子的問題,這跟設定畫面VCL物件無關

再來一般近代的程式,要是所有運算都在主執行處理,畫面會卡住,甚至點個幾次就停止回應了,所以其實一個不會當掉的程式,必須把運算量大的都丟到背景執行緒做,C++ Builder有提供TThread,而如果您不用這個就必須使用CreateThread來建立執行緒,當然Win32最近好像又多了ThreadPool及Command Thread這有空可以研究一下,總之運算量大的都要丟到背景就是了

而Windows程式有一個Windows訊息傳遞的寫法必須要會,當您丟到背景去處理運算後,處理結果要秀出到畫面,必須呼叫主執行緒跑(背景執行緒不能動到跟UI有關的東西),這裡不講這個,總之這也是必須要會的

最後就是Wndproc是處理Form的消息的地方,當您送一個訊息給Form,他會被Wndproc所處理,這裡的寫法很重要,後面有時間會提到這裡,這裡只是簡單寫一下而已

這裡會附上面的例子,您可以用C++ Builder打開測試看看
https://drive.google.com/open?id=0B6u5oifJv3EKOWFGdHZEdDl1cms

C++class間的繼承關係研究方法


http://hatsukiakio.blogspot.tw/2009/05/cclassstruct.html
大概是上面網頁所寫的

基本上我們在做繼承時,通常只會繼承一次,避免程式碼會太複雜

繼承太多層反而會使得您維護程式變得更困難,例如您寫了很多個class都有函式叫做abc(),結果您要用繼承的方式將很多class整合成一個class,就會發生abc()衝突,

要處理好幾層您必須每一層都仔細檢查及規劃,就一般在上班而言,一個案子通常會有時間壓力,不太可能會有時間去玩這種繼承好幾次的東西(除非您做的超級大型(像是C++ builder),但通常這種大型的東西,不是一個人能維護的)

一般就是做一個通用的class,將他放在基底,
然後您要使用時,就將它繼承出來,利用virtual來改寫操作
這裡就還不講這個,這裡主要就是教您如何研究這種基底關係,

其實就如上面網頁寫的,繼承基本上就只是那樣子

但是發文不附圖,沒圖沒真相,沒看到實際的東西,您怎麼確定人家寫的是正確的

其實很簡單

您要研究這種關係,就寫兩個class Aclass B

如下:
class A
{
public:
        A(){};
        ~A(){};
        int a;
protected:
        int b;
private:
        int c;
};

class B : public A
{
public:
        B(){
        };
        ~B(){};
protected:
   int d;
private:
};
//---------------------------------------------------------------------------
__fastcall TForm2::TForm2(TComponent* Owner)
        : TForm(Owner)
{
        B b;
        b.
}

這是一個最簡單的繼承關係,一般現在windowsIDE都會有提示功能,
如上我是將class B繼承class A
您在主程式片段就宣告個class B,然後輸入B.(如果是指標就是B->),他就會列出您可以存取的變數


class A裡面只有放int apublic,所以您要在class外直接存取變數,您看到只有a可以選擇


那假設您要在class B裡面的成員,直接使用變數,您可以使用this->(this就是指自己本身),看有誰可以看的到






基本上在研究時可以這樣簡單的看網路上教的對不對,要是您還不相信,就compiler看看,看是不是正確的,總之compiler完並且link成功,程式碼執行完沒跳錯誤,就代表是對的

但是通常案子一寫大,有時候include太多東西,或者是太多行,C++builder有時候就不會跳出這種提示出來,所以研究完還是需要記一下

再來

如上面的圖,您可將class B : private A,或者class B : protected A,並用上面的方法,看他的成員是否可以存取,或者是說在哪裡可以存取,並且編譯看看是否可以使用,用這種方法可以研究繼承的關係

通常假設很趕時,有時候您要使用成員看不到,您就將該成員丟到public底下,基本上是可以直接使用的,不然其實挺麻煩的,您還要像書上特別多寫一個函式,int a_get(){return a;},來取得該變數的值,或者是取得成員的指標,如果您要設定的話還要寫a_set(int value),通常除非我時間太多,或者公司有硬性規定或者已經寫好了,不然我就會向我說的將成員丟到public,就直接使用了,

多一個函式存取變數要多4行,要是您有10個變數要存取,不就要多寫40行,案子一大每個地方都這樣管理,其實一個人做實在是有點太累了,當然如果要做程式碼管理的話,還是必須乖乖的寫那些東西拉,不過如果沒上級特殊要求,我通常是會這樣處理,因為多一個函數轉換不會帶給您寫code有比較大的好處,反而會讓您的案子進展比較慢(因為要寫很多行)

當然要是您功能都開發完了,您要做程式碼管理,我是認為OK的,

但是要是您這只是小案子,其實過個幾天您就換案子維護了,做這種多餘的事其實很無聊的,就算是大案子是我的話,我通常也是先丟pullic寫好程式後,再看看要不要封裝成上面講的那樣寫法(不然寫錯了要改好幾次)

:D當然,寫Code是為了賺錢,所以案子越快完成越好,您要做任何一項動作時,除非這個動作帶給您什麼好處,例如寫Code比較快啦,修改程式碼比較有架構啦,這樣根據這個好處,才會對程式碼做有好處的動作,

這很像您上班時,公司要您打很多報告(為了管理),假設您主要工作內容是修電腦,你把報告打完或者是做的很棒,您電腦會修得比較多台嗎,

假設修一台電腦可以賺1000塊,然後您一直修可以修10台,就賺了一萬塊
要是您加上打報告時間,說不定只完成5台,您就少賺了5000塊了,

不然乾脆就裝5台,您還有時間去喝杯咖啡,或者是下午茶,總之做任何動作通常要帶來一定的好處,這好處指不是人家賦予的(例如請您順便打報告付您2萬塊)

而是實際會對您的工作效率實質上的提升,

一般還在學習時,做太多的東西做管理,您會學得慢很多(當然要是您是學管理就另外講了)

總之,這文章只是要教您如何來研究這種繼承的關係而已,其他閒談就自己判斷了,繼承用法網路上其實都已經有寫了,我這邊再打一次其實沒有太大的意義