norains的专栏

只专注于WINCE开发

用户操作
[即时聊天] [发私信] [加为好友]
norainsID:norains
130821次访问,排名642(-1)好友0人,关注者26
代码其实是一种乐趣
norains的文章
原创 186 篇
翻译 0 篇
转载 10 篇
评论 257 篇
norains的公告
联系方式请看置顶文章
最近评论
晴天:請問一下:
如果要寫成Watchdog timer 納在這這要如何實現呢?
hustpanda:电子书看不了呢?
bobo:“耍大牌”...... 你就该直接拉黑
bulrush:你好,首先先感谢一下。我看了你的音量控制,自己也去实现了一下,但是我个人感觉“AudioUpdateFromRegistry”没有依据注册表的设置来更新控制面板的音量。没有马上更新,我重启系统后才看到更新的结果。也就是说这种方法是可行,但是必须要重启,显然这不合理。上面的兄弟说:引用了这两个类后不起作用
如:
void CSoundDlg::OnSoft()
……
manyanxinlv:尽管我还小 但是 我还是看看
文章分类
收藏
    相册
    动漫
    文章图片
    程序交流
    xumercury的BLOG
    狗友们的博客
    清蒸石斑鱼
    美女如刀锋
    茁茁的BLOG
    魅力老姐的窝
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创 CTextWnd轻松实现文字的滚动收藏

    新一篇: 歌词显示的技术实现 | 旧一篇: 文字滚动的技术实现

    //========================================================================
    //TITLE:
    //    CTextWnd轻松实现文字的滚动
    //AUTHOR:
    //    norains
    //DATE:
    //    Wednesday  27-February-2008
    //Environment:
    //    VS2005  + SDK-WINCE5.0-MIPSII
    //    EVC 4.0 + SDK-WINCE5.0-MIPSII
    //========================================================================

        本文是我另一篇《文字滚动的技术实现》的实例补充,因为在该文已经详细解释技术实现的原理,所以本文的重点仅仅是如何调用CTextWnd实现文字的滚动。
       
        首先我们来通览一遍CTextWnd的完整源代码: 


    //////////////////////////////////////////////////////////////////////    
    // TextWnd.h: interface for the CTextWnd class.
    //
    //Version:
    //    0.1.4
    //
    //Date:
    //    2008.02.27
    //
    //Description:
    //    The base window version:
    //        CWndBase        -    0.1.8
    //        CMemDC            -    0.1.0
    //        CStrStore        -    1.0.0
    //////////////////////////////////////////////////////////////////////

    #pragma once
    #include 
    "wndbase.h"
    #include 
    "StrStore.h"
    #include 
    "MemDC.h"

    //----------------------------------------------------------
    //Enum value type
    enum DirectionValue
    {
        DRT_NULL,
        DRT_LEFT,
        DRT_RIGHT,
        DRT_UP,
        DRT_DOWN
    };
    //----------------------------------------------------------
    class CTextWnd :
        
    public CWndBase
    {
        
    public:
        
    virtual ~CTextWnd(void);
        CTextWnd(
    void);

        
    virtual BOOL Create(HINSTANCE hInst, HWND hWndParent, const TCHAR *pcszWndClass, const TCHAR *pcszWndName,BOOL bMsgThrdInside = FALSE);

        BOOL Move(
    const RECT * prcWnd);
        BOOL SetText(
    const TCHAR * pcszText);
        BOOL SetTxtPath(
    const TCHAR * pcszPath);
        BOOL Play(
    void);
        BOOL Pause(
    void);
        BOOL Stop(
    void);
        
    void SetDirection(DirectionValue dtValue);
        
    void SetInterval(DWORD dwInterval);
        
    void SetMovePixel(int iPixel);
        
    void SetTxtColor(COLORREF crColor);
        
    void SetTxtPointSize(int iPointSize);
        
    void SetTxtWeight(int iWeight);
        
    void SetBkColor(COLORREF crColor);
        BOOL SwitchNext(
    void);
        BOOL SwitchPrevious(
    void);


    private:
        
    static BOOL CheckFile(const TCHAR * pszFileName);
        BOOL ReadCurTxt(
    void);
        
    void OnPaint(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam);
        
    void OnWindowPosChanged(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam);
        BOOL ResetTxtInfoRect(
    void);    
        BOOL InitDCTxtInfo(
    void);
        BOOL FindFile(
    const TCHAR *pszPath,CStrStore *pStore,BOOL (*pCheckFunc)(const TCHAR *pcszPath));

    private:
        
    //Callback function
        virtual LRESULT WndProc(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam);
        
    virtual void DrawBackground(HDC hdc);

    private:
        CStrStore m_FileStore;
        
    int m_iIndexCurTxt;
        TCHAR 
    *m_pszTxtInfo;
        
    int m_iWndWidth;
        
    int m_iWndHeight;
        
    int m_iTxtInfoX;
        
    int m_iTxtInfoY;
        
    int m_iTxtInfoWidth;
        
    int m_iTxtInfoHeight;
        
    int m_iMovePixel;
        
    int m_iTxtInfoPointSize;    
        COLORREF m_crTxtInfoColor;
        COLORREF m_crBkColor;
        
    int m_iTxtInfoWeight;
        CMemDC m_DCTxtInfo;
        BOOL m_bInited;

        

    private:    
        
    //The value type is for the TimerThread,meaning the action
        enum TimeoutAction
        {
            TA_NULL,
            TA_MOVE,
            TA_EXIT,
            TA_STOP
        };
        HANDLE m_hEventTimer;
        TimeoutAction m_taCurAction;
        DWORD m_dwInterval;
        DirectionValue m_dtValue;
        
    static DWORD TimerThread(PVOID pArg);    
        
    };

    /////////////////////////////////////////////////////////////////////
    // TextWnd.cpp
    ////////////////////////////////////////////////////////////////////
    #include "stdafx.h"
    #include "TextWnd.h"


    //----------------------------------------------------------------------
    //Macro define
    #define DEFAULT_BKGND_COLOR  RGB(128,128,128)

    //For the text move
    #define DEFAULT_INTERVAL  100 //500ms
    #define DEFAULT_MOVE_PIXEL  1 //pixel

    //The text information
    #define DEFAULT_TEXT_COLOR  RGB(255,255,255)
    #define DEFAULT_TEXT_POINTSIZE  0
    #define DEFAULT_TEXT_WEIGHT  0
    //----------------------------------------------------------------------


    //----------------------------------------------------------------------
    //Description:
    // Construction
    //-----------------------------------------------------------------------
    CTextWnd::CTextWnd(void):
    m_iIndexCurTxt(0),
    m_pszTxtInfo(NULL),
    m_taCurAction(TA_NULL),
    m_dwInterval(DEFAULT_INTERVAL),
    m_hEventTimer(CreateEvent(NULL,FALSE,FALSE,NULL)),
    m_dtValue(DRT_NULL),
    m_iWndWidth(0),
    m_iWndHeight(0),
    m_iTxtInfoX(0),
    m_iTxtInfoY(0),
    m_iTxtInfoWidth(0),
    m_iTxtInfoHeight(0),
    m_iMovePixel(DEFAULT_MOVE_PIXEL),
    m_iTxtInfoPointSize(DEFAULT_TEXT_POINTSIZE), 
    m_crTxtInfoColor(DEFAULT_TEXT_COLOR),
    m_iTxtInfoWeight(DEFAULT_TEXT_WEIGHT),
    m_bInited(FALSE),
    m_crBkColor(DEFAULT_BKGND_COLOR)

     
     //Create the timer thread to move the text 
     HANDLE hdThrd = CreateThread(NULL,NULL,TimerThread,this,NULL,NULL);
     CloseHandle(hdThrd);
    }

    //----------------------------------------------------------------------
    //Description:
    // Destruction
    //----------------------------------------------------------------------
    CTextWnd::~CTextWnd(void)
    {
     if(m_hEventTimer != NULL)
     {
      CloseHandle(m_hEventTimer);
      m_hEventTimer = NULL;
     }

     if(m_DCTxtInfo.IsOK() == TRUE)
     {
      m_DCTxtInfo.Delete();
     }
    }

     


    //----------------------------------------------------------------------
    //Description:
    // Move the window
    //----------------------------------------------------------------------
    BOOL CTextWnd::Move(const RECT * prcWnd)
    {
     return MoveWindow(GetWindow(),
          prcWnd->left,
          prcWnd->top,
          prcWnd->right - prcWnd->left,
          prcWnd->bottom - prcWnd->top,
          FALSE);
    }

    //----------------------------------------------------------------------
    //Description:
    // Set the text information.
    // It would take effect in next calling Play().
    //----------------------------------------------------------------------
    BOOL CTextWnd::SetText(const TCHAR * pcszText)
    {
     if(m_pszTxtInfo != NULL)
     {
      delete [] m_pszTxtInfo;
      m_pszTxtInfo = NULL;
     }

     m_pszTxtInfo = new TCHAR[_tcslen(pcszText) + 1];
     _tcscpy(m_pszTxtInfo,pcszText);


     //Set the flag to reinitialize the memory DC
     m_bInited = FALSE;

     return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    // Set the text file path
    //----------------------------------------------------------------------
    BOOL CTextWnd::SetTxtPath(const TCHAR * pcszPath)
    {

     if(pcszPath == NULL)
     {
      return FALSE;
     }

     m_FileStore.DeleteAllData();

     WIN32_FIND_DATA fd = {0};
     HANDLE hFind = FindFirstFile(pcszPath,&fd);
     if(hFind == INVALID_HANDLE_VALUE)
     {
      return FALSE;
     }
     
     if(fd.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY)
     {
      FindFile(pcszPath,&m_FileStore,CheckFile);
     }
     else
     {
      m_FileStore.Add(pcszPath);
     }

     FindClose(hFind);

     //Open the first text file 
     m_iIndexCurTxt = 0;
     ReadCurTxt();

     return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    // Play the text. If first using, it would be very slow bacause must initialize the memory DC.
    // It would be quickly next time when the setting has not changed since first using
    //----------------------------------------------------------------------
    BOOL CTextWnd::Play(void)
    {
     if(m_DCTxtInfo.IsOK() == FALSE || m_bInited == FALSE)
     {
      //Create the memory DC for text information  
      InitDCTxtInfo();

      m_bInited = TRUE;
     }

     m_taCurAction = TA_MOVE;
     SetEvent(m_hEventTimer);

     return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    // Pause the text
    //----------------------------------------------------------------------
    BOOL CTextWnd::Pause(void)
    {
     m_taCurAction = TA_NULL;
     SetEvent(m_hEventTimer);

     return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    // Check the file
    //----------------------------------------------------------------------
    BOOL CTextWnd::CheckFile(const TCHAR * pszFileName)
    {
     TCHAR szSuffix[MAX_PATH] = {0};
     
     int iLen = _tcslen(pszFileName);
     if(iLen == 0)
     {
      return FALSE;
     }

     int iPos = iLen - 1;
     while(iPos >= 0)
     {
      if(pszFileName[iPos] == '.')
      {
       break;
      }

      iPos --;
     }

     if(iPos < 0)
     {
      return FALSE;
     }

     _tcscpy(szSuffix, pszFileName + iPos);
     _tcslwr(szSuffix);
     

     if(_tcscmp(szSuffix,TEXT(".txt")) == 0)
     {
      return TRUE;
     }

     return FALSE;
    }


    //----------------------------------------------------------------------
    //Description:
    // Read the text information base on the current index
    //----------------------------------------------------------------------
    BOOL CTextWnd::ReadCurTxt(void)
    {
     if(m_iIndexCurTxt >= m_FileStore.GetAmount() || m_iIndexCurTxt < 0 || m_FileStore.GetAmount() == 0)
     {
      return FALSE;
     }

     if(m_pszTxtInfo != NULL)
     {
      delete [] m_pszTxtInfo;
      m_pszTxtInfo = NULL;
     }

     TCHAR szFile[MAX_PATH] = {0};
     if(m_FileStore.GetData(m_iIndexCurTxt,szFile,MAX_PATH) == TRUE)
     {
      HANDLE hFile = CreateFile(szFile,
             GENERIC_READ,
             FILE_SHARE_READ,
             NULL,
             OPEN_EXISTING,
             FILE_ATTRIBUTE_NORMAL,
             NULL);

      DWORD dwSize = GetFileSize(hFile,NULL);
      BYTE *pReadBuf = (BYTE*)malloc(dwSize);
      DWORD dwRead = 0;
      ReadFile(hFile,pReadBuf,dwSize,&dwRead,NULL);
    #ifdef UNICODE
      if(dwSize >= 2 && pReadBuf[0] == 0xFF && pReadBuf[1] == 0xFE)
      {
       //It must be the UNICODE file
       DWORD dwNum = (dwSize - 2) / sizeof(TCHAR) + 1; //Must be end with '\0', so add 1
       m_pszTxtInfo = new TCHAR[dwNum];
       memset(m_pszTxtInfo,0,sizeof(TCHAR) * dwNum);
       if(m_pszTxtInfo != NULL)
       {
        _tcscpy(m_pszTxtInfo,reinterpret_cast<TCHAR *>(pReadBuf + 2));
       }   
       
      }
      else
      {
       //It must be the ASCII file
       DWORD dwNum = MultiByteToWideChar (CP_ACP, 0, reinterpret_cast<char *>(pReadBuf), -1, NULL, 0);
       m_pszTxtInfo = new TCHAR[dwNum];
       memset(m_pszTxtInfo,0,sizeof(TCHAR) * dwNum);
       if(m_pszTxtInfo != NULL)
       {
        // Convert string from ASCII to Unicode.
        MultiByteToWideChar (CP_ACP, 0, reinterpret_cast<char *>(pReadBuf), -1, m_pszTxtInfo, dwNum);
       }   
      }
      
     
    #else
     #error "There is not the completed code here when the UNICODE macor is not defined. :-("
    #endif

      free(pReadBuf);

      CloseHandle(hFile);
     }

     
     

     return TRUE;
    }

     

    //----------------------------------------------------------------------
    //Description:
    //    Actual WndProc
    //
    //----------------------------------------------------------------------
    LRESULT CTextWnd::WndProc(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
    {
     switch(wMsg)
     {
      case WM_PAINT:
       OnPaint(hWnd,wMsg,wParam,lParam);
       return 0;
      case WM_ERASEBKGND:
       return 0;
      case WM_WINDOWPOSCHANGED:
       OnWindowPosChanged(hWnd,wMsg,wParam,lParam);
       break;
     }

     return CWndBase::WndProc(hWnd,wMsg,wParam,lParam);
    }


    //----------------------------------------------------------------------
    //Description:
    //    On message WM_PAINT
    //
    //----------------------------------------------------------------------
    void CTextWnd::OnPaint(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
    {


     PAINTSTRUCT ps;
     HDC hdc = BeginPaint(hWnd,&ps);

     //Create the memory DC
     CMemDC memDC;
     SIZE size = {m_iWndWidth,m_iWndHeight};
     memDC.Create(hdc,&size);

     DrawBackground(memDC.GetDC());
     
     //Draw the text
     if(m_DCTxtInfo.IsOK() == TRUE)
     {
      TransparentBlt(memDC.GetDC(),
          m_iTxtInfoX,
          m_iTxtInfoY,
          m_iTxtInfoWidth,
          m_iTxtInfoHeight,
          m_DCTxtInfo.GetDC(),
          0,
          0,
          m_DCTxtInfo.GetWidth(),
          m_DCTxtInfo.GetHeight(),
          m_crBkColor);
     }
     

     //Draw the device context
     BitBlt(hdc,0,0,m_iWndWidth,m_iWndHeight,memDC.GetDC(),0,0,SRCCOPY);

     //Delete the memory DC
     memDC.Delete();
     
     EndPaint(hWnd,&ps);
    }

     

    //----------------------------------------------------------------------
    //Description:
    //    The timer thread
    //
    //----------------------------------------------------------------------
    DWORD CTextWnd::TimerThread(PVOID pArg)
    {
     CTextWnd *pObject = (CTextWnd *)pArg;

     

     while(TRUE)
     {
      if(WaitForSingleObject(pObject->m_hEventTimer,pObject->m_dwInterval) == WAIT_TIMEOUT)
      {
       if(pObject->m_taCurAction == TA_MOVE)
       {
        switch(pObject->m_dtValue)
        {
         case DRT_NULL:
         {
          //Do nothing
          break;
         }
         case DRT_LEFT:
         {
          pObject->m_iTxtInfoX -= pObject->m_iMovePixel;  
          if(pObject->m_iTxtInfoX + pObject->m_iTxtInfoWidth <= 0)
          {
           pObject->m_iTxtInfoX = pObject->m_iWndWidth;
          }
          break;
         }
         case DRT_RIGHT:
         {
          pObject->m_iTxtInfoX += pObject->m_iMovePixel; 
          if(pObject->m_iTxtInfoX >= pObject->m_iWndWidth)
          {
           pObject->m_iTxtInfoX =  - pObject->m_iTxtInfoWidth;
          }
          break;
         }
         case DRT_UP:
         {
          pObject->m_iTxtInfoY -= pObject->m_iMovePixel;
          if(pObject->m_iTxtInfoY + pObject->m_iTxtInfoHeight <= 0)
          {
           pObject->m_iTxtInfoY = pObject->m_iWndHeight;
          }
          break;
         }
         case DRT_DOWN:
         {
          pObject->m_iTxtInfoY += pObject->m_iMovePixel;
          if(pObject->m_iTxtInfoY >= pObject->m_iWndHeight)
          {
           pObject->m_iTxtInfoY = -pObject->m_iTxtInfoWidth;
          }
          break;
         }
        }
        
        //Redraw the text information
        InvalidateRect(pObject->GetWindow(),NULL,FALSE);     

       }
       else if(pObject->m_taCurAction == TA_EXIT)
       {
        break;
       }
       else if(pObject->m_taCurAction == TA_STOP)
       {
        switch(pObject->m_dtValue)
        {
         case DRT_NULL:
         {
          //Do nothing
          break;
         }
         case DRT_LEFT:
         {   
          pObject->m_iTxtInfoX = pObject->m_iWndWidth;   
          break;
         }
         case DRT_RIGHT:
         {
          pObject->m_iTxtInfoX =  - pObject->m_iTxtInfoWidth;   
          break;
         }
         case DRT_UP:
         {   
          pObject->m_iTxtInfoY = pObject->m_iWndHeight;  
          break;
         }
         case DRT_DOWN:
         {
          pObject->m_iTxtInfoY = -pObject->m_iTxtInfoWidth;    
          break;
         }
        }

        pObject->m_taCurAction = TA_NULL;
        InvalidateRect(pObject->GetWindow(),NULL,FALSE);
       }
      }
     }

     

     return 0;
    }

    //----------------------------------------------------------------------
    //Description:
    //    Set the direction for the text information displayed
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetDirection(DirectionValue dtValue)
    {
     if(m_dtValue == dtValue)
     {
      //Do nothing
      return;
     }

     m_dtValue = dtValue;

    }
    //----------------------------------------------------------------------
    //Description:
    //    Set the interval for text moving
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetInterval(DWORD dwInterval)
    {
     m_dwInterval = dwInterval;
    }


    //----------------------------------------------------------------------
    //Description:
    // Create the window
    //
    //Parameters:
    // You should see the explanation of the parameters in CWndBase
    //
    //----------------------------------------------------------------------
    BOOL CTextWnd::Create(HINSTANCE hInst, HWND hWndParent, const TCHAR *pcszWndClass, const TCHAR *pcszWndName,BOOL bMsgThrdInside)
    {
     if(CWndBase::Create(hInst,hWndParent,pcszWndClass,pcszWndName,bMsgThrdInside) == FALSE)
     {
      return FALSE;
     }

     //Get the window area
     RECT rcWnd = {0};
     GetWindowRect(GetWindow(),&rcWnd);
     m_iWndWidth = rcWnd.right - rcWnd.left;
     m_iWndHeight = rcWnd.bottom - rcWnd.top;

     return TRUE;
    }

    //----------------------------------------------------------------------
    //Description:
    // On message WM_WINDOWPOSCHANGED
    //
    //----------------------------------------------------------------------
    void CTextWnd::OnWindowPosChanged(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
    {
     //Get the window area
     WINDOWPOS wp = *((WINDOWPOS *) lParam);
     
     if(m_iWndWidth != wp.cx || m_iWndHeight != wp.cy)
     {
      m_iWndWidth = wp.cx;
      m_iWndHeight = wp.cy;

      //Set the flag to reinitialize the memory DC
      m_bInited = FALSE;
      
     }

     

    }


    //----------------------------------------------------------------------
    //Description:
    // Caculate the text informat rectangle.
    //
    //----------------------------------------------------------------------
    BOOL CTextWnd::ResetTxtInfoRect(void)
    {
     HWND hWnd = GetWindow();
     if(hWnd == NULL || m_pszTxtInfo == NULL)
     {
      return FALSE;
     }


     
     HDC hdc = GetDC(hWnd); 
     
     //Set the font to caculate
     LOGFONT lf = {0};
     HFONT hFontNew, hFontOld;
     lf.lfQuality = CLEARTYPE_QUALITY;
     lf.lfCharSet = DEFAULT_CHARSET;
     lf.lfHeight = -1 * (m_iTxtInfoPointSize * GetDeviceCaps(hdc,LOGPIXELSY) / 72);
     lf.lfWeight = m_iTxtInfoWeight;
     
     //Create the new font
     hFontNew = CreateFontIndirect(&lf);
     hFontOld = (HFONT) SelectObject(hdc, hFontNew);

     //Get the size of the string
     SIZE size = {0};
     GetTextExtentPoint(hdc,m_pszTxtInfo,_tcslen(m_pszTxtInfo),&size);

     //Restore the font
     SelectObject(hdc, hFontOld);
     DeleteObject(hFontNew);
     
     ReleaseDC(hWnd,hdc);

     

     if(m_dtValue == DRT_NULL)
     {
      m_iTxtInfoWidth = m_iWndWidth;
      m_iTxtInfoHeight = m_iWndHeight;
     }
     else if(m_dtValue == DRT_LEFT || m_dtValue == DRT_RIGHT)
     {
      m_iTxtInfoWidth = size.cx;
      m_iTxtInfoHeight = size.cy;
     }
     else if(m_dtValue == DRT_UP || m_dtValue == DRT_DOWN)
     {
      //Get the amount of the "\n"
      TCHAR *pFind = m_pszTxtInfo;
      int iCountLine = 1;
      while(TRUE)
      {
       pFind = _tcsstr(pFind,TEXT("\n"));
       
       if(pFind != NULL)
       {
        pFind += 1;
        iCountLine ++;
       }
       else
       {
        break;
       }
      }

      m_iTxtInfoWidth = m_iWndWidth;
      m_iTxtInfoHeight = size.cy * iCountLine;

     }

     //The begin position of X and Y
     switch(m_dtValue)
     {
      case DRT_NULL:
       m_iTxtInfoX = (m_iWndWidth - m_iTxtInfoWidth) / 2;
       m_iTxtInfoY = (m_iWndHeight - m_iTxtInfoHeight) / 2;
       break;
      case DRT_LEFT:
       m_iTxtInfoX = m_iWndWidth;
       m_iTxtInfoY = (m_iWndHeight - m_iTxtInfoHeight) / 2;
       break;
      case DRT_RIGHT:
       m_iTxtInfoX = -m_iTxtInfoWidth;
       m_iTxtInfoY = (m_iWndHeight - m_iTxtInfoHeight) / 2;
       break;
      case DRT_UP:
       m_iTxtInfoX = (m_iWndWidth - m_iTxtInfoWidth) / 2;
       m_iTxtInfoY = m_iWndHeight;
       break;
      case DRT_DOWN:
       m_iTxtInfoX = (m_iWndWidth - m_iTxtInfoWidth) / 2;
       m_iTxtInfoY = -m_iTxtInfoHeight;
       break;
     }

     return TRUE;
    }

    //----------------------------------------------------------------------
    //Description:
    // Draw the background
    //
    //----------------------------------------------------------------------
    void CTextWnd::DrawBackground(HDC hdc)
    {

     //Create the pen
     HPEN hPen = CreatePen(PS_SOLID,1,m_crBkColor);
     HPEN hOldPen = NULL;
     hOldPen = (HPEN)SelectObject(hdc,hPen);
     
     //the rect color
     HBRUSH hBrush = CreateSolidBrush(m_crBkColor);
     HGDIOBJ hOldBrush = SelectObject(hdc,hBrush);
     
     //Draw
     Rectangle(hdc,0,0,m_iWndWidth + 1, m_iWndHeight + 1);
     
     //Realse the resource
     SelectObject(hdc,hOldBrush);
     DeleteObject(hBrush);
     SelectObject(hdc,hOldPen);
     DeleteObject(hPen);

     
    }

     

    //----------------------------------------------------------------------
    //Description:
    // Initialize the memory DC for storing the current text information
    //
    //----------------------------------------------------------------------
    BOOL CTextWnd::InitDCTxtInfo(void)
    {
     if(ResetTxtInfoRect() == FALSE)
     {
      return FALSE;
     }

     HDC hdc = GetDC(GetWindow());

     //If existing the memory DC, delete it.
     m_DCTxtInfo.Delete();

     //Create the memory DC
     SIZE size = {m_iTxtInfoWidth,m_iTxtInfoHeight};
     m_DCTxtInfo.Create(hdc,&size);

     //Draw the background with the transparent color
     HPEN hPen = CreatePen(PS_SOLID,1,m_crBkColor);
     HPEN hOldPen = NULL;
     hOldPen = (HPEN)SelectObject(m_DCTxtInfo.GetDC(),hPen);
     //the rect color
     HBRUSH hBrush = CreateSolidBrush(m_crBkColor);
     HGDIOBJ hOldBrush = SelectObject(m_DCTxtInfo.GetDC(),hBrush);
     //Draw
     Rectangle(m_DCTxtInfo.GetDC(),0,0,m_iTxtInfoWidth + 1, m_iTxtInfoHeight + 1);
     //Realse the resource
     SelectObject(m_DCTxtInfo.GetDC(),hOldBrush);
     DeleteObject(hBrush);
     SelectObject(m_DCTxtInfo.GetDC(),hOldPen);
     DeleteObject(hPen);
     
     //Set the background mode
     ::SetBkMode(m_DCTxtInfo.GetDC(),TRANSPARENT);

     //Draw the text
     RECT rcDraw = {0, 0, m_iTxtInfoWidth, m_iTxtInfoHeight};
     UINT uFormat = 0;
     switch(m_dtValue)
     {
      case DRT_NULL:
       uFormat = DT_LEFT | DT_TOP | DT_WORDBREAK;
       break;
      case DRT_LEFT:
       uFormat = DT_VCENTER | DT_SINGLELINE | DT_LEFT;
       break;
      case DRT_RIGHT:
       uFormat = DT_VCENTER | DT_SINGLELINE | DT_RIGHT;
       break;
      case DRT_UP:
      case DRT_DOWN:
       uFormat = DT_CENTER | DT_WORDBREAK;
       break;
     }

     //Set the text color
     COLORREF crOldTextColor = ::SetTextColor(m_DCTxtInfo.GetDC(),m_crTxtInfoColor);

     //Set the font to caculate
     LOGFONT lf = {0};
     HFONT hFontNew, hFontOld;
     lf.lfQuality = CLEARTYPE_QUALITY;
     lf.lfCharSet = DEFAULT_CHARSET;
     lf.lfHeight = -1 * (m_iTxtInfoPointSize * GetDeviceCaps(m_DCTxtInfo.GetDC(),LOGPIXELSY) / 72);
     lf.lfWeight = m_iTxtInfoWeight;
     
     //Create the new font
     hFontNew = CreateFontIndirect(&lf);
     hFontOld = (HFONT) SelectObject(m_DCTxtInfo.GetDC(), hFontNew);

     if(m_dtValue == DRT_RIGHT)
     {
      //Reverse the string
      int iLen = _tcslen(m_pszTxtInfo);
      TCHAR *pszReverse = new TCHAR[iLen + 1];
      for(int i = 0; i < iLen; i ++)
      {
       pszReverse[i] = m_pszTxtInfo[iLen - i - 1];
      }
      pszReverse[iLen] = '\0';

      DrawText(m_DCTxtInfo.GetDC(),pszReverse,-1,&rcDraw,uFormat);

      delete []pszReverse;
     }
     else
     {
      DrawText(m_DCTxtInfo.GetDC(),m_pszTxtInfo,-1,&rcDraw,uFormat);
     }

     //Restore the font
     SelectObject(m_DCTxtInfo.GetDC(), hFontOld);
     DeleteObject(hFontNew);

     //Restore the color
     ::SetTextColor(m_DCTxtInfo.GetDC(),crOldTextColor);

     ReleaseDC(GetWindow(),hdc);

     return TRUE;
     
    }

    //----------------------------------------------------------------------
    //Description:
    // Set the move distance once, base on pixel.
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetMovePixel(int iPixel)
    {
     m_iMovePixel = iPixel;
    }


    //----------------------------------------------------------------------
    //Description:
    // Set the text color. Don't set the same color with the background.
    // It would take effect in next calling Play().
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetTxtColor(COLORREF crColor)
    {
     m_crTxtInfoColor = crColor;

     //Set the flag to reinitialize the memory DC
     m_bInited = FALSE;
     
    }

    //----------------------------------------------------------------------
    //Description:
    // Set the text point size.
    // It would take effect in next calling Play().
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetTxtPointSize(int iPointSize)
    {
     m_iTxtInfoPointSize = iPointSize;

     //Set the flag to reinitialize the memory DC
     m_bInited = FALSE;
    }

    //----------------------------------------------------------------------
    //Description:
    // Set the text weight. See LOGFONT for a list of value.
    // It would take effect in next calling Play().
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetTxtWeight(int iWeight)
    {
     m_iTxtInfoWeight = iWeight;

     //Set the flag to reinitialize the memory DC
     m_bInited = FALSE;
    }

    //----------------------------------------------------------------------
    //Description:
    // Set the color for the background of window
    //
    //----------------------------------------------------------------------
    void CTextWnd::SetBkColor(COLORREF crColor)
    {
     m_crBkColor = crColor;
    }

    //----------------------------------------------------------------------
    //Description:
    // Switch to next image
    //----------------------------------------------------------------------
    BOOL CTextWnd::SwitchNext(void)
    {
     if(m_FileStore.GetAmount() == 0)
     {
      return FALSE;
     }

     m_iIndexCurTxt ++;
     if(m_iIndexCurTxt >= m_FileStore.GetAmount())
     {
      m_iIndexCurTxt = 0;
     }

      
     ReadCurTxt();

     return TRUE;
    }

    //----------------------------------------------------------------------
    //Description:
    // Switch to previous image
    //----------------------------------------------------------------------
    BOOL CTextWnd::SwitchPrevious(void)
    {
     if(m_FileStore.GetAmount() == 0)
     {
      return FALSE;
     }

     m_iIndexCurTxt --;
     if(m_iIndexCurTxt < 0 )
     {
      m_iIndexCurTxt = m_FileStore.GetAmount() - 1;
     }

      
     ReadCurTxt();

     return TRUE;
    }

     


    //---------------------------------------------------------------------
    //Description:
    // Find the file in the specified directory
    //
    //Parameters:
    // pszPath : [in] The path to find file
    // pStore : [in] The buffer for storing the file path
    // pCheckFunc : [in] Pointer to the function for checking the file
    //---------------------------------------------------------------------
    BOOL CTextWnd::FindFile(const TCHAR *pszPath,CStrStore *pStore,BOOL (*pCheckFunc)(const TCHAR *pcszPath))
    {
     if(_tcslen(pszPath) >= MAX_PATH || pszPath == NULL || pStore == NULL || pCheckFunc == NULL)
     {
      return FALSE;
     }

     
     TCHAR szFindDir[MAX_PATH] = {0};
     _tcscpy(szFindDir,pszPath);
     //the last 4 elememts must be like as "\*.*"
     ULONG ulLen = _tcslen(szFindDir);
     if(ulLen != 0 && szFindDir[ulLen - 1]=='\\')
     {
      _tcscat(szFindDir,TEXT("*.*"));
     } 
     else
     {
      _tcscat(szFindDir,TEXT("\\*.*"));
     }


     WIN32_FIND_DATA fd;
     HANDLE hFind;
     hFind=FindFirstFile(szFindDir,&fd);
     if(hFind != INVALID_HANDLE_VALUE)
     {
      do{
       if(fd.dwFileAttributes==FILE_ATTRIBUTE_DIRECTORY)
       {  
        //it must be directory
        TCHAR szNextDir[MAX_PATH] = {0};
        _tcscpy(szNextDir,pszPath);
        ULONG ulLenNext = _tcslen(pszPath);
        if(ulLenNext != 0 && szNextDir[ulLenNext - 1] != '\\')
        {
         _tcscat(szNextDir,TEXT("\\"));
        }
        _tcscat(szNextDir,fd.cFileName);
        FindFile(szNextDir,pStore,pCheckFunc);
       }
       else
       { 
        //it is file
        
        if((*pCheckFunc)(fd.cFileName) == TRUE)
        {
         TCHAR szPathFile[MAX_PATH] = {0};
         _tcscpy(szPathFile,pszPath);
         ULONG ulLenFile = _tcslen(szPathFile);
         if(ulLenFile != 0 && szPathFile[ulLenFile - 1] != '\\')
         {
          _tcscat(szPathFile,TEXT("\\"));
         }
         _tcscat(szPathFile,fd.cFileName);
         pStore->Add(szPathFile);
        }
        
       }
      }while(FindNextFile(hFind,&fd));
     }
     FindClose(hFind);

     return TRUE;
    }

    //---------------------------------------------------------------------
    //Description:
    // Stop playing
    //
    //---------------------------------------------------------------------
    BOOL CTextWnd::Stop(void)
    {
     m_taCurAction = TA_STOP;
     SetEvent(m_hEventTimer);

     return TRUE;
    }


        在CTextWnd中还用到了三个类,分别是CMemDC、CWndBase和CStrStore,其作用依次是创建内存DC、父类窗口和存储文件路径。
       
        CWndBase的相关信息可参考这边文章:http://blog.csdn.net/norains/archive/2007/11/10/1878218.aspx
       
        CMemDC和CStrStore的代码比较简单,没有什么难点需要描述的,其代码分别如下:
       
       
        CMemDC:

    //////////////////////////////////////////////////////////////////////
    // MemDC.h: interface for the CCommon class.
    //
    //Version:
    //    0.1.0
    //Date:
    //    2008.02.20
    //////////////////////////////////////////////////////////////////////

    #pragma once

    class CMemDC
    {
    public:
        CMemDC(
    void);
        
    ~CMemDC(void);

        BOOL Create(HDC hdc, 
    const SIZE *pSize);
        BOOL Delete(
    void);
        HDC GetDC(
    void);
        LONG GetWidth(
    void);
        LONG GetHeight(
    void);
        BOOL IsOK(
    void);


    private:
        HDC m_hdcMem;
        HBITMAP m_hBitmap;
        HGDIOBJ m_hOldSel;
        SIZE m_Size;

        
    };


    //////////////////////////////////////////////////////////////////////
    // MemDC.cpp
    //
    //////////////////////////////////////////////////////////////////////
    #include "stdafx.h"
    #include 
    "MemDC.h"

    //----------------------------------------------------------------------
    //Description:
    //    Construction
    //-----------------------------------------------------------------------
    CMemDC::CMemDC(void):
    m_hdcMem(NULL),
    m_hBitmap(NULL),
    m_hOldSel(NULL)
    {
        memset(
    &m_Size,0,sizeof(m_Size));
    }

    //----------------------------------------------------------------------
    //Description:
    //    Destruction
    //-----------------------------------------------------------------------
    CMemDC::~CMemDC(void)
    {
    }


    //----------------------------------------------------------------------
    //Description:
    //    Create the memory DC. After succeed in calling the function ,you should
    //call Delete() to release resource.
    //
    //Parameters:
    //    hdc : [in] Handle to an existing device context. 
    //    pSize : [in] The size of the memory DC to create.
    //        If NULL, the function uses the screen size.
    //-----------------------------------------------------------------------
    BOOL CMemDC::Create(HDC hdc, const SIZE *pSize)
    {
        BOOL bResult 
    = FALSE;

        
    if(hdc == NULL || m_hdcMem != NULL)
        {
            
    goto EXIT;
        }

        
    if(pSize != NULL)
        {
            m_Size 
    = *pSize;
        }
        
    else
        {
            m_Size.cx 
    = GetSystemMetrics(SM_CXSCREEN);
            m_Size.cy 
    = GetSystemMetrics(SM_CYSCREEN);
        }

        
    //Create a DC that matches the device
        m_hdcMem = CreateCompatibleDC(hdc);
        
    if(m_hdcMem == NULL)
        {
            
    goto EXIT;
        }

        m_hBitmap 
    = CreateCompatibleBitmap(hdc,m_Size.cx,m_Size.cy);
        
    if(m_hBitmap == NULL)
        {
            
    goto EXIT;
        }

        
    //Select the bitmap into to the compatible device context
        m_hOldSel = SelectObject(m_hdcMem,m_hBitmap);

        bResult 
    = TRUE;

    EXIT:
        
    if(bResult == FALSE)
        {
            DeleteObject(m_hBitmap);
            m_hBitmap 
    = NULL;
            
            DeleteDC(m_hdcMem);
            m_hdcMem 
    = NULL;
        }

        
    return bResult;
    }



    //----------------------------------------------------------------------
    //Description:
    //    Delete the memory DC
    //
    //-----------------------------------------------------------------------
    BOOL CMemDC::Delete(void)
    {
        
    if(m_hdcMem == NULL || m_hOldSel == NULL || m_hBitmap == NULL)
        {
            
    return FALSE;
        }

        
    //Restore original bitmap selection and destroy the memory DC
        SelectObject(m_hdcMem,m_hOldSel);
        DeleteObject(m_hBitmap);    
        DeleteDC(m_hdcMem);

        m_hdcMem 
    = NULL;
        m_hOldSel 
    = NULL;
        m_hBitmap 
    = NULL;

        memset(
    &m_Size,0,sizeof(m_Size));

        
    return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Get the handle of the memory DC. You COULDN'T release the DC by window api funtion ReleaseDC() !
    //Instead, you should call CMemDC::Delete().
    //    Null incdicates failed
    //
    //-----------------------------------------------------------------------
    HDC CMemDC::GetDC(void)
    {
        
    return m_hdcMem;
    }

    //----------------------------------------------------------------------
    //Description:
    //    Get the width of the memory DC
    //
    //-----------------------------------------------------------------------
    LONG CMemDC::GetWidth(void)
    {
        
    return m_Size.cx;
    }

    //----------------------------------------------------------------------
    //Description:
    //    Get the height of the memory DC
    //
    //-----------------------------------------------------------------------
    LONG CMemDC::GetHeight(void)
    {
        
    return m_Size.cy;
    }

    //----------------------------------------------------------------------
    //Description:
    //    Check the memory DC. 
    //
    //Parameters:
    //    NULL
    //
    //Return Values:
    //    TRUE - ready.
    //    FALSE - Not ready.
    //
    //-----------------------------------------------------------------------
    BOOL CMemDC::IsOK(void)
    {
        
    return (m_hdcMem != NULL);
    }

        CStrStore:   
    //////////////////////////////////////////////////////////////////////
    // StrStore.h: interface for the CStrStore class.
    //
    //Version:
    //    1.0.0
    //Date:
    //    2008.02.26
    //////////////////////////////////////////////////////////////////////

    #pragma once



    //-----------------------------------------------------------------------
    class CStrStore  
    {
    public:
        
    int GetDataLength(int iIndex);
        
    int FindData(const TCHAR *pcszFind);
        BOOL GetData(
    int iIndex,TCHAR *pszOut, int iSize);
        
    int GetAmount();
        
    void DeleteAllData();
        BOOL Add(
    const TCHAR * pszIn);
        CStrStore();
        
    virtual ~CStrStore();
    protected:

        typedef 
    struct _StoreData
        {
            TCHAR 
    *pszString;
            _StoreData 
    *pNextData;
        }STOREDATA,
    *PSTOREDATA;
        PSTOREDATA m_pFirstData;
        PSTOREDATA m_pEndData;

        
    int m_iAmount;

    };


    //////////////////////////////////////////////////////////////////////
    // StrStore.cpp: implementation of the CStrStore class.
    //
    //////////////////////////////////////////////////////////////////////

    #include 
    "stdafx.h"
    #include 
    "StrStore.h"

    //////////////////////////////////////////////////////////////////////
    // Construction/Destruction
    //////////////////////////////////////////////////////////////////////

    CStrStore::CStrStore()
    {
        m_pFirstData 
    = NULL;
        m_pEndData 
    = NULL;
        m_iAmount 
    = 0;
    }

    CStrStore::
    ~CStrStore()
    {
        DeleteAllData();
    }


    //----------------------------------------------------------------------
    //Description:
    //    Add the TCHAR string
    //----------------------------------------------------------------------
    BOOL CStrStore::Add(const TCHAR * pszIn)
    {
        PSTOREDATA pNewData 
    = new STOREDATA();
        
        
    if(pNewData == NULL)
        {
            
    return FALSE;
        }

        
    int iLen = _tcslen(pszIn);
        pNewData
    ->pszString = new TCHAR [iLen + 1];
        
    if(pNewData->pszString == NULL)
        {
            delete pNewData;
            
    return FALSE;
        }
        _tcscpy(pNewData
    ->pszString,pszIn);
        
        
        pNewData
    ->pNextData = NULL;

        
    if(m_pFirstData == NULL)
        {
            m_pFirstData 
    = pNewData;
        }
        
    else
        {
            m_pEndData
    ->pNextData = pNewData;
        }

        m_pEndData 
    = pNewData;

        m_iAmount 
    ++;

        
    return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Delete all the stored data
    //----------------------------------------------------------------------
    void CStrStore::DeleteAllData()
    {
        
    if(m_pFirstData == NULL)
        {
            
    return ;
        }

        PSTOREDATA pDeleteData 
    = m_pFirstData;

        
    while(m_pFirstData->pNextData != NULL)
        {
            m_pFirstData 
    = m_pFirstData->pNextData;

            delete []pDeleteData
    ->pszString;
            delete pDeleteData;
            
            pDeleteData 
    = m_pFirstData;
        }

        
    //Delete the last one
        delete pDeleteData;

        m_pFirstData 
    = NULL;
        m_pEndData 
    = NULL;

        m_iAmount 
    = 0;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Get the amount
    //----------------------------------------------------------------------
    int CStrStore::GetAmount()
    {
        
    return m_iAmount;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Get the data base on the index. And the begin index is 0.
    //----------------------------------------------------------------------
    BOOL CStrStore::GetData(int iIndex, TCHAR *pszOut, int iSize)
    {
        
    if(iIndex < 0 || iIndex >= m_iAmount || pszOut == NULL)
        {
            
    return FALSE;
        }

        
    int iCount = 0;
        PSTOREDATA pData 
    = m_pFirstData;

        
    while(iCount != iIndex)
        {
            pData 
    = pData->pNextData;
            iCount 
    ++;
        }

        
    int iLen = _tcslen(pData->pszString);
        
        
    if(iSize < iLen + 1)
        {
            
    return FALSE;
        }

        _tcscpy(pszOut,pData
    ->pszString);


        
    return TRUE;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Find the string data and return the index in the storage.
    //
    //Parameters:
    //    pcszFind: [in] The string to find
    //
    //Return Values:
    //    -1 : failed
    //    others: succeed.
    //----------------------------------------------------------------------
    int CStrStore::FindData(const TCHAR *pcszFind)
    {
        
    int iIndexFind = 0;


        PSTOREDATA pData 
    = m_pFirstData;

        
    while(iIndexFind < m_iAmount)
        {
            
    if(_tcscmp(pData->pszString,pcszFind) == 0)
            {
                
    break;
            }

            pData 
    = pData->pNextData;
            iIndexFind 
    ++;
        }

        
    if(iIndexFind >= m_iAmount)
        {
            
    //Failed in finding the string
            iIndexFind = -1;
        }

        
    return iIndexFind;
    }


    //----------------------------------------------------------------------
    //Description:
    //    Get the length of the string data base on the index. And the begin index is 0.
    //
    //Parameters:
    //    iIndex : [in] The index in the stored data,and it's base on 0.
    //
    //Return Values:
    //    -1 : failed
    //    others: succeed.
    //----------------------------------------------------------------------
    int CStrStore::GetDataLength(int iIndex)
    {
        
    if(iIndex < 0 || iIndex >= m_iAmount)
        {
            
    return -1;
        }

        
    int iCount = 0;
        PSTOREDATA pData 
    = m_pFirstData;

        
    while(iCount != iIndex)
        {
            pData 
    = pData->pNextData;
            iCount 
    ++;
        }

        
    return  _tcslen(pData->pszString);

    }

      
        既然前戏已经过完,那么我们开始来干正活吧。 :-)
       
        如果我们想创建一个窗口,该窗口的文字有下往上滚动,则代码可以如此:
    int WINAPI WinMain(    HINSTANCE hInstance,
                        HINSTANCE hPrevInstance,
                        LPTSTR    lpCmdLine,
                        
    int       nCmdShow)
    {
         
    // TODO: Place code here.

        CTextWnd txtWnd;
        txtWnd.Create(hInstance,NULL,TEXT(
    "TextWnd"),TEXT("TextWnd"));
        txtWnd.SetDirection(DRT_UP);
        txtWnd.SetText(TEXT(
    "向上滚动第二行信息"));
        txtWnd.Play();
        txtWnd.ShowWindow(TRUE);

        MSG msg;
        
    while(GetMessage(&msg,NULL,0,0))
        {
            TranslateMessage(
    &msg);
            DispatchMessage(
    &msg);
        }

        
    return 0;
    }


       
        一个滚动窗口就出来了,是不是很简单?
       
        最后,我们来看看CTextWnd一些常用的函数:
       
       
        1.Create(HINSTANCE hInst, HWND hWndParent, const TCHAR *pcszWndClass, const TCHAR *pcszWndName,BOOL bMsgThrdInside = FALSE)
          在使用之前都必须创建一个窗口。
         
        2.SetText(const TCHAR * pcszText)
          设置显示的文本
         
        3.SetDirection(DirectionValue dtValue)
          设置文字的滚动方向,取值有五种:DRT_NULL,DRT_LEFT,DRT_RIGHT,DRT_UP,DRT_DOWN,其含义和字面意义一致。
         
        4.SetInterval(DWORD dwInterval)
          设置移动的时间间隔,默认为100ms
         
        5.SetMovePixel(int iPixel)
          设置每次移动的像素,默认为1
         
        6.SetTxtPath(const TCHAR * pcszPath)
          设置读取的文件名或文件夹。如果所给的路径为文件夹,则该函数默认将读取第一个文件,但可以通过调用SwitchNext和SwitchPrevious切换不同的文件。
          
       7.Play(void)
         开始播放。
        
         这里还有个函数需要留意的,CTextWnd会调用DrawBackground进行背景的绘制。因为在实际使用中,往往需要实现透明的效果或是绘制其它的背景画面,这时候只要创建一个派生自CTextWnd的类,然后重载DrawBackground函数即可。

        该文的例子可在此下载:http://download.csdn.net/source/362836

    发表于 @ 2008年02月27日 21:28:00|评论(loading...)|编辑

    新一篇: 歌词显示的技术实现 | 旧一篇: 文字滚动的技术实现

    评论

    #book 发表于2008-02-28 10:31:30  IP: 218.0.4.*
    学习了,很强.TextWnd.cpp不完整啊.
    2008-02-28 11:03:21作者回复
    已修正。PS:不能不佩服CSDN BLOG开发组,代码一长就给我毫不留情给割断,一点提示都没有。这已经不是第一次了 :-(
    发表评论  


    登录
    Csdn Blog version 3.1a
    Copyright © norains