從畢業到現在已經有比較長的一段時間了,在這些工作期間,編寫工控軟件一直是我主要從事的職業。由於當時所在的是一個中小心公司,主要負責電力行業的監控系統,同時開拓一些工控相關的業務,比如自來水,閘門,橡膠壩等系統。這類系統一個顯著的特點就是系統集成度比較高,要求對現場的控制要求比較了解,然後采用PLC+工控機的模式就可以了。現在的PLC品種繁多,而且技術都非常成熟,能夠穩定運行,且各PLC廠商都提供了編程軟件,能夠快速的進行開發,而在上位機(工控機軟件)部分也有不少的組態軟件提供商,比較有名的諸如組態王、Visual Graph、iFix等。但是這些組態軟件很多都是根據項目授權,即投運一個項目需要得到他的一個授權。作為中小心公司,公司領導層覺得既然擁有自己的軟件開發工程師如果再采用組態軟件來實現的話是一筆很劃不來的事情,所以公司的主張就是自己開發自己的監控軟件。
開發工控軟件很重要的一個部分就是圖形系統。很多組態系統提供了豐富的圖形組態功能,能夠通過他們提供的圖形庫完成各種復雜的圖形開發,而且效果非常好。比如表記,LED,趨勢,流動控件等。而自己開發則要單調很多。現在有有了一些諸如世紀飛揚等控件提供商,提供一系列控件開發包,不過一句話,他們都是要花米米的。作為很多開發人員是不願意去出這些錢的,但是搜索網絡上面這類控件,卻幾乎難得找到幾個。前些時候一個朋友接了一個項目,一個泵廠的監控系統,這個系統實現中需要很多的管道和管道內部趨勢圖,能夠實時反映水流的方向等。由於屬於私人開發不可能花錢去購買那些控件,於是要我給他做一個可用於這個項目能夠反映流動情況的管道控件。
多虧了現在網絡上信息的豐富,“如果說我成功那是因為我站在了巨人的肩膀上“,通過考慮,我們可以采用管道和流動相結合的方法來實現我們的控件
首先是管道,管道簡單考慮就是橫和豎兩種(至於您有時間可以考慮那些七七八八轉彎的情況),那麼我們這裡可以把管道定義成兩種模式
typedef enum _FlowPattern
{
FP_HORIZONTAL = 0,
FP_VERTICAL = 1
}FLOWPATTERN;
然後是裡面的水流的流動方向,橫向的可以為由左向右和由右向左,豎向的可以分為由上向下和右下向上,這樣我們就可以抽象裡面的流動方向:typedef enum _DirectionType
{
LEFT_TO_RIGHT = 0,
RIGHT_TO_LEFT = 1,
TOP_TO_BOTTOM = 2,
BOTTOM_TO_TOP = 3
}DIRECTIONTYPE;
下面就是管道的繪制了,如何繪制才能達到視覺上的管道效果呢。如果是一個平面矩形是不能看起來視覺效果的管道的。參考了網絡上面的一些漸變的處理方法後發現,其實我們的管道可以用這個方法來實現。如果管道的邊和中央用兩種不同的顏色,這兩種顏色為一種顏色近似的值,一種深,一種淺。當顏色處理由邊向中央繪制采用的方法為:淺—深—淺時,這樣我們可以產生視覺效果的管道的凹表面,反過來則能夠產生管道的凸表面。現在網絡上面如何進行漸變繪圖的方法實例很多,這個是沒有問題的。
然後就是我們要考慮的流動的設計了。在我們平常見到過的一些流動控件中(收費控件)流動的形狀可以為多種,有的是矩形,有的是小三角形等。而流動的表現就是在管道的中間繪制一些我們上面舉例的小圖形等。這樣,我們為這些單個的小圖形設計一個功能類,然後在類中間實現不同的單個小滑塊進行繪制,如果我們的管道有N個這樣的小滑塊組成,如果我們進行動態的繪制小滑塊,那麼不就會產生視覺上的滑動了。
原理諸如上面所需,我們在滑動的控件中設計一個定時器,利用定時器驅動,然後將管道利用形狀動態填充一個滑塊列隊,每次動態計算每個滑塊的位置,通過動態刷新管道背景和滑塊位置(擦除掉原來的那些舊的滑塊位置)就可以產生滑動的效果了,管道流動控件原理基本上就是這樣了。
下面是動手實現了,我們首先通過ClassWizard創建一個繼承CStatic的管道流動類CFlowCtrl,然後映射WM_PAINT消息,因為在這裡要完成管道和滑塊的繪制。如果我們的定時器時間很小,刷新頻率很快,將會出現很嚴重的閃爍現象,這裡我們采用雙緩沖,在內存中間進行復雜的圖形的繪制,然後進行“貼“(BitBtn)到界面上就可以不閃爍了。為了使程序的可讀性增強,我們對管道繪制和滑塊繪制分兩個類(CFlowImpl、CPipeImpl)來進行。
管道繪制:
采用漸變的方法進行管道的繪制,由於在繪圖過程中,這裡不是經常變化的,只有當管道的顏色發生變化或者其他一些管道的配置發生變化才要重繪,我們在雙緩沖繪圖時只要進行一次繪圖就可以了,然後生成繪圖句柄到內存,這樣可以提高效率。
void CPipeImpl::Draw(CDC *pDC)
{
//用漸變的方法繪制管道
DrawGradient(pDC);
//繪制圖形周圍的邊框
CRect rcClient;
m_pOwer->GetClientRect(&rcClient);
CBrush pBlackBrush(m_colorBorder);
pDC->FrameRect(&rcClient, &pBlackBrush);
}
繪制漸變我們采用一個常用的方法,這裡進行這個方法的簡單的封裝,以後還能夠用這些方法繪制圖形的漸變區域以及工業控制別的控件,比如容器等。void CPipeImpl::DrawGradient(CDC *pDC)
{
CRect rcClient;
CBitmap MemBmp, *pOldMemBmp;
CDC MemDC;
COLORREF crCur;
CPoint ptCur;
m_pOwer->GetClientRect(&rcClient);
int nWidth = m_pOwer->GetFlowPattern() == FP_HORIZONTAL ?
rcClient.Height() : rcClient.Width();
RGBTRIPLE *pRGBTriple = new RGBTRIPLE[nWidth], *pEntry;
if (pRGBTriple == NULL)
{
return;
}
MemBmp.CreateCompatibleBitmap(pDC,
m_pOwer->GetFlowPattern() == FP_HORIZONTAL ? 1 : rcClient.Width(),
m_pOwer->GetFlowPattern() == FP_HORIZONTAL ? rcClient.Height() : 1);
MemDC.CreateCompatibleDC(pDC);
pOldMemBmp = (CBitmap *)MemDC.SelectObject(&MemBmp);
ptCur = CPoint(0, 0);
m_Gradient.MakeEntries(pRGBTriple, nWidth);
for (int i=0; i<nWidth; i++)
{
if (m_pOwer->GetFlowPattern() == FP_HORIZONTAL)
{
ptCur.y = i;
}
else
{
ptCur.x = i;
}
pEntry = &pRGBTriple[i];
crCur = RGB(pEntry->rgbtRed, pEntry->rgbtGreen, pEntry->rgbtBlue);
MemDC.SetPixelV(ptCur, crCur);
}
if (m_pOwer->GetFlowPattern() == FP_HORIZONTAL)
{
pDC->StretchBlt(rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(),
&MemDC, 0, 0, 1, nWidth, SRCCOPY);
}
else
{
pDC->StretchBlt(rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(),
&MemDC, 0, 0, nWidth, 1, SRCCOPY);
}
MemDC.SelectObject(pOldMemBmp);
MemBmp.DeleteObject();
MemDC.DeleteDC();
delete [] pRGBTriple;
}
FlowCtrl在OnPaint函數中通過調用PipeImpl的Draw就能夠完成背景的繪制了。下面是滑塊的繪制,根據開始的原理,我們對每個滑塊抽象一個小類CFlowUnit,根據滑塊的形狀我們可以抽象滑塊的外觀:
typedef enum _UnitPattern
{
UP_RECTANGLE = 0,
UP_CIRCLE = 1,
UP_DLINE = 2, // 象 >> 類別
UP_TRIANGLE = 3
}UNITPATTERN;
然後實現一個Draw函數完成這些不同形狀的滑塊的繪制工作。
void CFlowUnit::Draw(CDC *pDC, CRect &rcClient)
{
CBrush pBackBrush, *pOldBrush;
if (m_pParentWnd == NULL)
{
return;
}
pBackBrush.CreateSolidBrush(m_pParentWnd->GetUnitBackColor());
pOldBrush = (CBrush *)pDC->SelectObject(&pBackBrush);
switch (m_pParentWnd->GetUnitPattern())
{
case UP_RECTANGLE:
pDC->FillRect(&m_rectClient, &pBackBrush);
break;
case UP_CIRCLE:
pDC->SelectObject(::GetStockObject(NULL_PEN));
pDC->Ellipse(&m_rectClient);
break;
case UP_DLINE:
{
int xCenter = 0, yCenter = 0;
if ((m_pParentWnd->GetUnitDLinePattern() == DP_RIGHT) || (m_pParentWnd->GetUnitDLinePattern() == DP_LEFT))
{
xCenter = m_rectClient.left + (m_rectClient.right - m_rectClient.left) / 2;
yCenter = (rcClient.bottom - rcClient.top) / 2;
}
else
{
xCenter = (rcClient.right - rcClient.left) / 2;
yCenter = m_rectClient.top + (m_rectClient.bottom - m_rectClient.top) / 2;
}
DrawDLine(pDC, CPoint(xCenter, yCenter));
}
break;
case UP_TRIANGLE:
pDC->SelectObject(::GetStockObject(NULL_PEN));
DrawTriangle(pDC);
break;
}
pDC->SelectObject(pOldBrush);
pBackBrush.DeleteObject();
}
void CFlowUnit::DrawDLine(CDC *pDC, CPoint ptCenter)
{
if (m_pParentWnd == NULL)
{
return;
}
switch (m_pParentWnd->GetUnitDLinePattern())
{
case DP_RIGHT:
_D_LINE_H(ptCenter.x + 0, ptCenter.y + 0, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 1, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 2, ptCenter.y + 2, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 1, ptCenter.y + 3, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 0, ptCenter.y + 4, m_pParentWnd->GetUnitBackColor());
break;
case DP_LEFT:
_D_LINE_H(ptCenter.x + 2, ptCenter.y + 0, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 1, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 0, ptCenter.y + 2, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 1, ptCenter.y + 3, m_pParentWnd->GetUnitBackColor());
_D_LINE_H(ptCenter.x + 2, ptCenter.y + 4, m_pParentWnd->GetUnitBackColor());
break;
case DP_DOWN:
_D_LINE_V(ptCenter.x + 0, ptCenter.y + 0, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 1, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 2, ptCenter.y + 2, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 3, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 4, ptCenter.y + 0, m_pParentWnd->GetUnitBackColor());
break;
case DP_UP:
_D_LINE_V(ptCenter.x + 0, ptCenter.y + 2, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 1, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 2, ptCenter.y + 0, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 3, ptCenter.y + 1, m_pParentWnd->GetUnitBackColor());
_D_LINE_V(ptCenter.x + 4, ptCenter.y + 2, m_pParentWnd->GetUnitBackColor());
break;
default:
break;
}
}
void CFlowUnit::DrawTriangle(CDC *pDC)
{
if (m_pParentWnd == NULL)
{
return;
}
CPoint ptRgn[3];
switch (m_pParentWnd->GetFlowCtrl()->GetDirectionType())
{
case LEFT_TO_RIGHT:
ptRgn[0].x = m_rectClient.left;
ptRgn[0].y = m_rectClient.top;
ptRgn[1].x = m_rectClient.left;
ptRgn[1].y = m_rectClient.bottom;
ptRgn[2].x = m_rectClient.right;
ptRgn[2].y = m_rectClient.top + (m_rectClient.bottom - m_rectClient.top) / 2;
break;
case RIGHT_TO_LEFT:
ptRgn[0].x = m_rectClient.right;
ptRgn[0].y = m_rectClient.top;
ptRgn[1].x = m_rectClient.right;
ptRgn[1].y = m_rectClient.bottom;
ptRgn[2].x = m_rectClient.left;
ptRgn[2].y = m_rectClient.top + (m_rectClient.bottom - m_rectClient.top) / 2;
break;
case TOP_TO_BOTTOM:
ptRgn[0].x = m_rectClient.left;
ptRgn[0].y = m_rectClient.top;
ptRgn[1].x = m_rectClient.right;
ptRgn[1].y = m_rectClient.top;
ptRgn[2].x = m_rectClient.left + (m_rectClient.right - m_rectClient.left) / 2;
ptRgn[2].y = m_rectClient.bottom;
break;
case BOTTOM_TO_TOP:
ptRgn[0].x = m_rectClient.left;
ptRgn[0].y = m_rectClient.bottom;
ptRgn[1].x = m_rectClient.right;
ptRgn[1].y = m_rectClient.bottom;
ptRgn[2].x = m_rectClient.left + (m_rectClient.right - m_rectClient.left) / 2;
ptRgn[2].y = m_rectClient.top;
break;
default:
break;
}
CBrush pBackBrush, *pOldBrush;
pBackBrush.CreateSolidBrush(m_pParentWnd->GetUnitBackColor());
pOldBrush = (CBrush *)pDC->SelectObject(&pBackBrush);
pDC->SetPolyFillMode(WINDING);
pDC->Polygon(ptRgn, 3);
pDC->SelectObject(pOldBrush);
pBackBrush.DeleteObject();
}
剩下來的就是我們實現滑塊的位置控制了。我們可以采用一個簡單的實現方法,比如采用每個定時器到達的時候移動幾個像素點的方法,通過向管道裡面的滑塊劉表傳遞下一個區域就可以了。當這個區域處於滑出去的區域時候,我們自動銷毀這個滑塊對象,然後重新添加一個滑塊對象到最開始的地方就可以了。 我們將在FlowImpl中定義一個CList<CFlowUnit *, CFlowUnit *> m_arrayFlowUnit;鏈表,然後在初始化的時候通過滑塊的長,寬等信息計算管道可以容納的滑塊數量和位置。void CFlowImpl::InitFlowUnit()
{
CRect rcUnit, rcClient;
m_pOwer->GetClientRect(&rcClient);
m_arrayFlowUnit.RemoveAll();
if (m_pOwer->GetDirectionType() == LEFT_TO_RIGHT
|| m_pOwer->GetDirectionType() == RIGHT_TO_LEFT)
{
int nUnits = rcClient.Width() / (UNIT_WIDTH + UNIT_DISC);
int nResidue = (rcClient.bottom - rcClient.top) % 2;
int yCenter = (rcClient.bottom - rcClient.top) / 2;
for (int i=0; i<nUnits; i++)
{
rcUnit.SetRect(rcClient.left + i * (UNIT_WIDTH + UNIT_DISC),
yCenter - UNIT_HEIGHT / 2,
rcClient.left + i * (UNIT_WIDTH + UNIT_DISC) + UNIT_WIDTH,
yCenter + UNIT_HEIGHT / 2 + nResidue);
CFlowUnit *pFlowUnit = new CFlowUnit();
pFlowUnit->SetRect(rcUnit);
pFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddTail(pFlowUnit);
}
}
else
{
int nUnits = rcClient.bottom / (UNIT_WIDTH + UNIT_DISC);
int nResidue = (rcClient.right - rcClient.left) % 2;
int xCenter = (rcClient.right - rcClient.left) / 2;
for (int i=0; i<nUnits; i++)
{
rcUnit.SetRect(xCenter - UNIT_HEIGHT / 2,
rcClient.top + i * (UNIT_WIDTH + UNIT_DISC),
xCenter + UNIT_HEIGHT / 2 + nResidue,
rcClient.top + i * (UNIT_WIDTH + UNIT_DISC) + UNIT_WIDTH);
CFlowUnit *pFlowUnit = new CFlowUnit();
pFlowUnit->SetRect(rcUnit);
pFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddTail(pFlowUnit);
}
}
}
然後通過定時器到達的時候動態計算鏈表滑塊的每個對象的下一個要顯示的CRect,然後進行管道背景刷新就可以完成了。
void CFlowImpl::CalculateFlowUnit()
{
CRect rcClient;
m_pOwer->GetClientRect(&rcClient);
int xResidue = (rcClient.right - rcClient.left) % 2;
int xCenter = (rcClient.right - rcClient.left) / 2;
int yCenter = (rcClient.bottom - rcClient.top) / 2;
int yResidue = (rcClient.bottom - rcClient.top) % 2;
POSITION pos = m_arrayFlowUnit.GetHeadPosition();
while (pos != NULL)
{
CFlowUnit *pFlowUnit = m_arrayFlowUnit.GetAt(pos);
//從左至右
if (m_pOwer->GetDirectionType() == LEFT_TO_RIGHT)
{
CRect rcUnit = pFlowUnit->GetRect();
rcUnit.DeflateRect(UNIT_STEP, 0, -UNIT_STEP, 0);
pFlowUnit->SetRect(rcUnit);
if (rcUnit.left >= rcClient.right)
{
pFlowUnit = m_arrayFlowUnit.GetHead();
CRect rcAddUnit;
rcAddUnit.SetRect(pFlowUnit->GetRect().left - UNIT_DISC - UNIT_WIDTH,
yCenter - UNIT_HEIGHT / 2,
pFlowUnit->GetRect().left - UNIT_DISC,
yCenter + UNIT_HEIGHT / 2 + yResidue);
CFlowUnit *pAddFlowUnit = new CFlowUnit();
pAddFlowUnit->SetRect(rcAddUnit);
pAddFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddHead(pAddFlowUnit);
m_arrayFlowUnit.RemoveAt(pos);
break;
}
}
//從右至左
else if (m_pOwer->GetDirectionType() == RIGHT_TO_LEFT)
{
CRect rcUnit = pFlowUnit->GetRect();
rcUnit.DeflateRect(-UNIT_STEP, 0, +UNIT_STEP, 0);
pFlowUnit->SetRect(rcUnit);
if (rcUnit.right <= rcClient.left)
{
pFlowUnit = m_arrayFlowUnit.GetTail();
CRect rcAddUnit;
rcAddUnit.SetRect(pFlowUnit->GetRect().right + UNIT_DISC,
yCenter - UNIT_HEIGHT / 2,
pFlowUnit->GetRect().right + UNIT_DISC + UNIT_WIDTH,
yCenter + UNIT_HEIGHT / 2 + yResidue);
CFlowUnit *pAddFlowUnit = new CFlowUnit();
pAddFlowUnit->SetRect(rcAddUnit);
pAddFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddTail(pAddFlowUnit);
m_arrayFlowUnit.RemoveAt(pos);
break;
}
}
//從上至下
else if (m_pOwer->GetDirectionType() == TOP_TO_BOTTOM)
{
CRect rcUnit = pFlowUnit->GetRect();
rcUnit.DeflateRect(0, UNIT_STEP, 0, -UNIT_STEP);
pFlowUnit->SetRect(rcUnit);
if (rcUnit.top >= rcClient.bottom)
{
pFlowUnit = m_arrayFlowUnit.GetHead();
CRect rcAddUnit;
rcAddUnit.SetRect(xCenter - UNIT_HEIGHT / 2,
pFlowUnit->GetRect().top - UNIT_DISC - UNIT_WIDTH,
xCenter + UNIT_HEIGHT / 2 + xResidue,
pFlowUnit->GetRect().top - UNIT_DISC);
CFlowUnit *pAddFlowUnit = new CFlowUnit();
pAddFlowUnit->SetRect(rcAddUnit);
pAddFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddHead(pAddFlowUnit);
m_arrayFlowUnit.RemoveAt(pos);
break;
}
}
//從下至上
else if (m_pOwer->GetDirectionType() == BOTTOM_TO_TOP)
{
CRect rcUnit = pFlowUnit->GetRect();
rcUnit.DeflateRect(0, -UNIT_STEP, 0, +UNIT_STEP);
pFlowUnit->SetRect(rcUnit);
if (rcUnit.bottom <= rcClient.top)
{
pFlowUnit = m_arrayFlowUnit.GetTail();
CRect rcAddUnit;
rcAddUnit.SetRect(xCenter - UNIT_HEIGHT / 2,
pFlowUnit->GetRect().bottom + UNIT_DISC,
xCenter + UNIT_HEIGHT / 2 + xResidue,
pFlowUnit->GetRect().bottom + UNIT_DISC + UNIT_WIDTH);
CFlowUnit *pAddFlowUnit = new CFlowUnit();
pAddFlowUnit->SetRect(rcAddUnit);
pAddFlowUnit->SetParentWnd(this);
m_arrayFlowUnit.AddTail(pAddFlowUnit);
m_arrayFlowUnit.RemoveAt(pos);
break;
}
}
m_arrayFlowUnit.GetNext(pos);
}
}
最後我們定義一些控件的接口完成對控件的參數的設置。
我們可以看到程序的運行效果:
如果您還有更好的方法實現此類控件或者您已經實現了那些七七八八拐彎的功能,請您將您的經驗寫下來和我們分享。如果我們都能夠分享自己的一些編程經驗,那麼我們的軟件將會越來越人性化,越來越穩定,越來越好。
如有什麼問題和好的建議可以和我聯系:
QQ:5516853
本文配套源碼