• 窗体皮肤实现


    稍微改造一下,让交互性更好点。增加提示和动态效果。

    控件实现内容:

    • 1、加入Hint提示
    • 2、加入了简易动画效果,鼠标进入和离开会有个渐变效果。

    实现方案:

    • 1、基类选用
    • 2、Action的关联
    • 3、绘制按钮
    • 4、鼠标响应
    • 5、美化(淡入淡出简易动画) OK~完成

    一、基类选择

    在基类选择上稍微纠结了下。Delphi大家都知道做一个显示控件一般有2种情况,一种是图形控件(VC里叫静态控件),还种种有焦点可交互的。

    如果我想做个Toolbar并不需要焦点,也不需要处理键盘输入,TGraphicControl 是比较理想的继承类。不过最终还是使用了TWinControl,主要一点是TWinControl有个句柄方便处理。当然 TGraphicControl也是可以申请句柄的。这个问题就不纠结,确定使用TWinControl。

    二、关联Action

    说是关联其实就是Toolbar有多少个Button,需要保存这些Button的信息。在标题工具栏(四)中已经有简易实现。个人喜欢用Record来记录东西,简单方便不要管创建和释放。

    TmtToolItem = record
        Action: TBasicAction;  
        Enabled: boolean;
        Visible: boolean;
        ImageIndex: Word;         // 考虑到标题功能图标和实际工具栏功能使用不同图标情况,分开图标索引
        Width: Word;              // 实际占用宽度,考虑后续加不同的按钮样式使用
        Fade: Word;               // 褪色量 0 - 255
        SaveEvent: TNotifyEvent;  // 原始的Action OnChange事件
      end;

    这是一个Button的信息,记录了些基本的信息(这个和原来一样)。如果愿意可以加个样式类型(Style),来绘制更多的Button样式。

    TmtCustomToolbar = class(TWinControl)
    private
      FItems: array of TmtToolItem;
      FCount: Integer;
      ... ...

    FItems 和 FCount 用来记录Button的数组容器。直接使用SetLength动态设置数组的长度,简易不用创建直接使用。有了容器,Action就需要个入口来传入。

    处理三件事情:
    1、检测容器容量,不够增加
    2、清空第Count位的Record值(清零)。这步其实对Record比较重要,如果记录中增加参数值时...给你来个随机数那就比较郁闷了。
    3、填充记录
    4、重算尺寸并重新绘制

    procedure TmtCustomToolbar.Add(Action: TBasicAction; AImageIndex: Integer);
    begin
      if FCount >= Length(FItems) then
        SetLength(FItems, FCount + 5);
    
      // 保存Action信息
      ZeroMemory(@FItems[FCount], SizeOf(TmtToolItem));
      FItems[FCount].Action := Action;
      FItems[FCount].Enabled := true;
      FItems[FCount].Visible := true;
      FItems[FCount].ImageIndex := AImageIndex;
      FItems[FCount].Width := 20;
      FItems[FCount].Fade := 0;
      FItems[FCount].SaveEvent := TacAction(Action).OnChange;
      TacAction(Action).OnChange := DoOnActionChange;
    
      // 初始化状态
      with FItems[FCount] do
        if Action.InheritsFrom(TContainedAction) then
        begin
          Enabled := TContainedAction(Action).Enabled;
          Visible := TContainedAction(Action).Visible;
        end;
    
      inc(FCount);
    
      // 更新显示尺寸
      UpdateSize;    
    end;
    
    保存Action信息

    三、绘制按钮

    绘制肯定是要完全控制,画布画笔都必须牢牢的攥在手里。美与丑就的靠自己有多少艺术细胞。本人是只有艺术脓包,至于你信不信,反正我是信了。

    处理两个消息:WM_PaintWM_ERASEBKGND。不让父类(TWinControl)做多余的事情。

    WM_ERASEBKGND 处理背景擦除,这个不必处理。直接告诉消息,不处理此消息。

    procedure TmtCustomToolbar.WMEraseBkgnd(var message: TWMEraseBkgnd);
    begin
      Message.Result := 1;  // 已经处理完成了,不用再处理
    end;

    WM_Paint消息为减少闪烁,使用Buffer进行绘制。

    procedure TmtCustomToolbar.WMPaint(var message: TWMPaint);
    var
      DC, hPaintDC: HDC;
      cBuffer: TBitmap;
      PS: TPaintStruct;
      R: TRect;
      w, h: Integer;
    begin
      ///
      /// 绘制客户区域
      ///
      R := GetClientRect;
      w := R.Width;
      h := R.Height;
    
      DC := Message.DC;
      hPaintDC := DC;
      if DC = 0 then
        hPaintDC := BeginPaint(Handle, PS);
    
      // 创建个画布,在这个上面绘制。
      cBuffer := TBitmap.Create;  
      try
        cBuffer.SetSize(w, h);
        PaintBackground(cBuffer.Canvas.Handle);
        PaintWindow(cBuffer.Canvas.Handle);
        // 绘制完成的图形,直接拷贝到界面。这就是传说中的双缓冲技术木?
        BitBlt(hPaintDC, 0, 0, w, h, cBuffer.Canvas.Handle, 0, 0, SRCCOPY);
      finally
        cBuffer.free;
      end;
    
      if DC = 0 then
        EndPaint(Handle, PS);
    end;

    最有就是绘制界面上的Action。只要循环绘制完所有按钮就OK了

    处理过程:
    1、是否要绘制,隐藏跳过
    2、根据鼠标事件状态绘制按钮底纹。(按钮在Hot状态还是鼠标按下状态)
    3、获得Action的图标,在2的基础上绘制。

    OK~完成,偏移位置继续画下个。

    获取按钮的状态绘制,默认状态,按下状态和鼠标滑入的状态。

    function GetActionState(Idx: Integer): TSkinIndicator;
      begin
        Result := siInactive;   
        if (Idx = FPressedIndex) then
          Result := siPressed
        else if (Idx = FHotIndex) and (FPressedIndex = -1) then
          Result := siHover;
      end;

    具体绘制色块型的是非常简单,根据不同类型获取状态颜色。

    function GetColor(s: TSkinIndicator): Cardinal; inline;
      begin
        case s of
          siHover         : Result := SKINCOLOR_BTNHOT;
          siPressed       : Result := SKINCOLOR_BTNPRESSED;
          siSelected      : Result := SKINCOLOR_BTNPRESSED;
          siHoverSelected : Result := SKINCOLOR_BTNHOT;
        else                Result := SKINCOLOR_BTNHOT;
        end;
      end;

    然后就是直接填充颜色

    procedure DrawStyle(DC: HDC; const R: TRect; AColor: Cardinal); inline;
      var
        hB: HBRUSH;
      begin
        hB := CreateSolidBrush(AColor);
        FillRect(DC, R, hB);
        DeleteObject(hB);
      end;

    获得图标就不多说啦。直接根据Action的信息获得。

    这里主要注意的是,图标是有透明层。需要使用绘制透明函数AlphaBlend处理。

    class procedure TTreeViewSkin.DrawIcon(DC: HDC; R: TRect; ASrc: TBitmap; const
        Opacity: Byte = 255);
    var
      iXOff: Integer;
      iYOff: Integer;
    begin
      ///
      ///  绘制图标
      ///    绘制图标是会作居中处理
      iXOff := r.Left + (R.Right - R.Left - ASrc.Width) div 2;
      iYOff := r.Top + (r.Bottom - r.Top - ASrc.Height) div 2;
      DrawTransparentBitmap(ASrc, 0, 0, DC, iXOff, iYOff, ASrc.Width, ASrc.Height, Opacity);
    end;

    四、鼠标事件响应

    鼠标的响应,处理移动、按下、弹起。其他就不需要了。在鼠标移动时检测所在的按钮,按下是一样确定按下的是那个Button,弹开时执行Button的Action事件。不同状态的切换,需要告诉界面进行重新绘制。

    在鼠标移动时,除了检测所在按钮外。FHotIndex记录当前光标所在的按钮索引。如果没有按下的状态,需要告诉系统我要显示提示(Hint)。

    procedure TmtCustomToolbar.WMMouseMove(var message: TWMMouseMove);
    var
      iSave: Integer;
    begin
      iSave := FHotIndex;
      HotIndex := HitTest(message.XPos, message.YPos);
      // 在没有按下按钮时触发Hint显示
      if (iSave <> FHotIndex) and (FHotIndex >= 0) and  (FPressedIndex = -1) then
        Application.ActivateHint(message.Pos);  
    end;

    按下时检测,按下的那个按钮。FPressedIndex记录按下的按钮索引(就是数组索引)

    弹起时处理按钮事件。这里稍微需要处理一下,就是按下鼠标后不松开移动鼠标到其他地方~~ 结果~~。一般系统的处理方式是不执行那个先前被按下的按钮事件。

    所以在弹起时也要检测一下。原先按下的和现在的按钮是否一致,不一致就不处理Action。

    五、美化,加入简易动画效果

    为了能看起来不是很生硬,在进入按钮和离开时增加点动画效果。当然这个还是比较菜的效果。如果想很炫那就的现象一下,如何才能很炫。然后用你手里攥着的画笔涂鸦把!

    动画效果主要加入一个90毫秒的一个定时器,90毫秒刷一次界面~。这样就能感觉有点像动画的效果,要更加精细的话可以再短些。

    CONST
      TIMID_FADE = 1; // Action褪色
    
    procedure TmtCustomToolbar.SetHotIndex(const Value: Integer);
    begin
      if FHotIndex <> Value then
      begin
        FHotIndex := Value;
        Invalidate;
        // 鼠标的位置变了,启动定时器
        //   有Handle 就不用再独立创建一个Timer,可以启动很多个用ID区分。
        if not(csDestroying in ComponentState) and HandleAllocated then
          SetTimer(Handle, TIMID_FADE, 90, nil);
      end;
    end;

    到点刷新界面,在Timer中增加褪色刷新处理

    // 是褪色定时器,那么刷新界面
    if message.TimerID = TIMID_FADE then
      UpdateFade;

    褪色值其实就是一个0~255的一个透明Alpha通道值,每次绘制底色时根据这个阀值来绘制透明背景Button底纹。所有都为透明时,关闭动画时钟。

    procedure TmtCustomToolbar.UpdateFade;
    var
      I: Integer;
      bHas: boolean;
    begin
      bHas := False;
      for I := 0 to FCount - 1 do
        if FItems[I].Visible and FItems[I].Enabled then
        begin
          // 设置褪色值
          //   鼠标:当前Button,那么趋向不透明(255)
          //        不再当前位置,趋向透明(0)
          if FHotIndex = I then
            FItems[I].Fade := GetShowAlpha(FItems[I].Fade)
          else if FItems[I].Fade > 0 then
            FItems[I].Fade := GetFadeAlpha(FItems[I].Fade);
          bHas := bHas or (FItems[I].Fade > 0);
        end;
      Invalidate;
      if not bHas and HandleAllocated then
        KillTimer(Handle, TIMID_FADE);
    end;

    完成啦~

    工具条效果图

    这个简易Toolbar只实现了Button样式,没有分割线没有下拉多选之类的样式。

    ”这么弱的东西有毛用?“

    其实这个工具条主要目的是用于附着在其他控件上使用,比如某些控件的标题区域位置。当然如果想要搞的强大,那么代码量肯定会膨胀。但在实际项目中使用往往都比较简单,并不需要太过复杂的功能,和做通过控件是完全不同的概念。

    到此 窗体皮肤实现 的基本处理完结。客户区实际会备停靠框架挤占,所以客户木内容。

    后续会介绍一些其他控件的实现。由于某些原有,代码会直接使用C/C++写,不再使用Delphi

    --

    开发环境:

    • Delphi XE3
    • Win7

    完整源代码:

  • 相关阅读:
    js 中 && 和 ||
    The server time zone value 'EDT' is unrecognized or represents more than one time zone.
    docker进入容器
    docker 挂载本地目录
    Docker for Windows 挂载目录失败
    docker下redis启动 根据本地配置文件
    docker 安装 nacos/nacos-server 镜像并配置本地数据库
    spring cloud Nacos Config配置中心
    Docker卸载
    虚拟机centos添加硬盘和分区挂载
  • 原文地址:https://www.cnblogs.com/zhoug2020/p/5789034.html
Copyright © 2020-2023  润新知