吵吵   2013-10-27  阅读:2,504

delphi的vcl组件,吵吵已经玩的比较熟悉了,但是换到Lazarus后,我还是有些担心。对于一个开源的系统,如果选择的开发程序和框架错误的话,就很难有多大的发展了。

因此,我选择用lazarus来先练练手,做了一个LCL的可视化组件。将检验科常用的Levey-Jennings指控图做成了一个可视化的组件。具体效果来看,确实还不错。如果会用delphi的话,到lazarus就变得非常的简单,希望我们的选择是正确的吧。

实现的功能:

1、多水平的L-J指控图的绘制。

2、均值、SD值等的自动计算。

3、Hit的智能提示。传统的指控图对靠近SD线上的点的值无法判断是否超出,因此吵吵增加了一个Hit提示,当点击指控图上的任意个点的时候,可以有黄色提示窗显示该点的质控时间、数值、靶值以及该值相对于SD的Z分数值,例如-2.1SD即代表超出了-2SD的质控线,相对SD来讲Z分数为-2.1。

LJChart

编程思路

1、TLJChart组件继承于TCustomCtrol类,该类已经提供了Canvas对象来进行绘图了。

2、黄色提示框是使用自带的THintWindow类来做的,它提供了一些基本的函数,很好调用。

后话

还是半成品,只做实验用,正式版本稍后发布。

遇到问题:

对于多水平的质控数据,如何判定是同一次的质控?比如生化项目AST,有H,L两个水平的质控,LIS系统中得到的数据是按时间区分的,
H 2013-10-20 12:31 55.5,L 2013-10-20 12:32 35.5,这两个时间差距很小,但是确实是同一次的质控,当判定是2-2s规则失控的时候,是必须要算进去的,如果忽略时间,留下日期,如上所述的两次质控都是在2013-10-23日的,我就判定为同一次质控,当遇到重做质控,或者一天做多次质控的时候,又如何判断?

期待回音!

已下是源码,很乱,将就看看吧:


unit LJChart;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs,Types,Printers,math;

type
  QCPoint=record
    Time:TDatetime;
    Value:Double;
  end;

type
  QCLevel=record
    Title:string[50];
    AVG:Double;
    SD:Double;
    PointCount:Integer;
    Points:array of QCPoint;
  end;

type
  QCData=record
    Title:string[255];
    LevelCount:integer;
    Levels:array of QCLevel;
  end;
type
  GraphPoint=record
    x:integer;
    y:integer;
    r:integer;
    Value:Double;
    Time:TDateTime;
    RSD:Double;
    AVG:Double
  end;

type
  TCustomLJChart = class(TCustomControl)

  end;

type
  TLJChart = class(TCustomLJChart)
  private
    { Private declarations }
    FQCData:QCData;
    FHint:THintWindow;
    FPoints:array of GraphPoint;

  protected
    { Protected declarations }
    procedure DrawFrame();
    procedure DrawHeader(ACanvas:TCanvas;AData:QCData;ARect:TRect);
    procedure DrawChart(ACanvas:TCanvas;AData:QCData;ARect:TRect);
    procedure DrawAxis(ACanvas:TCanvas;ALevel:QCLevel;ARect:TRect);
    procedure DrawLine(ACanvas:TCanvas;ALevel:QCLevel;ARect:TRect);
    procedure AddGraphPoint(ALevel:QCLevel;ARect:TRect);
  protected
    procedure MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer); override;

  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    procedure paint;override;
    procedure Print(Index:integer);
  public
    procedure AddData(ADate:QCData);

  public
    property Data:QCData read FQCData write FQCData;
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  {$I ljchart_icon.lrs}
  RegisterComponents('Lab',[TLJChart]);
end;

constructor TLJChart.Create(AOwner: TComponent);

begin
  FHint:=THintWindow.Create(self);
  FHint.HideInterval:=5000;
  FHint.AutoHide:=true;

  inherited;
end;

procedure TLJChart.paint;
var
  i:integer;
  rect:TRect;
begin
  DrawFrame;
  if length(FQCData.Levels)=0 then exit;
  rect.Left:=0;
  rect.Top:=0;
  rect.Right:= Self.Width;
  rect.Bottom:=Self.Height;
  DrawChart(Self.Canvas,FQCData,rect);
end;

procedure TLJChart.MouseDown(Button: TMouseButton; Shift:TShiftState; X,Y:Integer);
var
  i:integer;
  Rect:TRect;
  tempStr:string;
  tempP:Tpoint;
  iWidth,iHeight:integer;
begin

  for i:=0 to Length(FPoints)-1 do
  begin
    if (x-FPoints[i].x)*(x-FPoints[i].x)+(y-FPoints[i].y)*(y-FPoints[i].y)<=FPoints[i].r*FPoints[i].r then
    begin
      tempStr:='';
      tempStr:=tempStr+'Time:'+DateTimeToStr(FPoints[i].Time)+char(13)+char(10);
      tempStr:=tempStr+'QC:'+FloatToStr(FPoints[i].value)+char(13)+char(10);

      tempStr:=tempStr+'RSD:'+FloatToStr(FPoints[i].RSD)+'SD'+char(13)+char(10);
      tempStr:=tempStr+'AVG:'+FloatToStr(FPoints[i].AVG);
      Rect := FHint.CalcHintRect(0,tempStr,nil);
      tempP:=Self.ClientToScreen(POINT(x,y));
      iWidth:=Rect.Right-Rect.Left;
      Rect.Left:=Rect.Left+tempP.x;
      Rect.Right:=Rect.Left+iWidth;
      iHeight:=Rect.Bottom-Rect.Top;
      Rect.Top:=Rect.Top+tempP.y;
      Rect.Bottom:=Rect.Top+iHeight;
      FHint.ActivateHint(Rect,tempStr);

    end;
  end;
end;

procedure TLJChart.AddData(ADate:QCData);
var
  i:integer;
  iTop:integer;
  iBottom:integer;
begin
  FQCData:=ADate;
  for i:=0 to FQCData.LevelCount-1 do
  begin
    iTop:= Round((0.2+i*0.8/FQCData.LevelCount)*Self.Height);
    iBottom:= Round((0.2+(i+1)*0.8/FQCData.LevelCount)*Self.Height);
    AddGraphPoint(FQCData.Levels[i],Rect(0,iTop,Self.Width,iBottom));

  end;
  paint;
end;

procedure TLJChart.DrawFrame();
begin
  Canvas.Pen.Color:=RGBToColor(0,0,0);
  Canvas.Brush.Color:=RGBToColor(255,255,255);
  Canvas.Rectangle(rect(0,0,Self.Width,Self.Height));

end;

procedure TLJChart.DrawChart(ACanvas:TCanvas;AData:QCData;ARect:TRect);
var
  i:integer;
  iTop:integer;
  iBottom:integer;
begin
  DrawHeader(ACanvas,AData,Rect(0,1,(ARect.Right-ARect.Left),Round((ARect.Bottom-ARect.Top)*0.2)));
  for i:=0 to AData.LevelCount-1 do
  begin
    iTop:= Round((0.2+i*0.8/AData.LevelCount)*(ARect.Bottom-ARect.Top));
    iBottom:= Round((0.2+(i+1)*0.8/AData.LevelCount)*(ARect.Bottom-ARect.Top));
    DrawAxis(ACanvas,AData.Levels[0],Rect(0,iTop,(ARect.Right-ARect.Left),iBottom));
    DrawLine(ACanvas,AData.Levels[0],Rect(0,iTop,(ARect.Right-ARect.Left),iBottom));

  end;
end;

procedure TLJChart.DrawHeader(ACanvas:TCanvas;AData:QCData;ARect:TRect);
var
  iTextHeight:Integer;
  iX,iY:Integer;
begin
  ACanvas.Font.Color:=RGBToColor(0,0,0);
  iTextHeight:=ACanvas.TextHeight('w');
  ACanvas.Font.Size:=Round((ARect.Bottom-ARect.Left)*0.25*ACanvas.Font.Size/iTextHeight);
  iX:=((ARect.Right-ARect.Left)-length(AData.Title)*ACanvas.TextWidth('w')) div 2;
  iY:=ARect.Top;
  ACanvas.TextOut(iX,iY,AData.Title);

end;

procedure TLJChart.DrawAxis(ACanvas:TCanvas;ALevel:QCLevel;ARect:TRect);
var
  i:Integer;
  a:Integer;
  tempStr:string;
  tempX,tempY:integer;
  tempInt:Integer;
begin
  //绘制横向坐标

  ACanvas.Font.Size:=12;
  ACanvas.Font.Size:=Round(((ARect.Bottom-ARect.Top)*0.05)*12/ACanvas.TextHeight('8'));
  for i := 0 to 8 do
  begin
    ACanvas.pen.Color:=RGBToColor(200,200,200);
    if i=4 then   ACanvas.pen.Color:=RGBToColor(0,0,0);

     if (i=2) or (i=6) then ACanvas.pen.Color:=RGBToColor(255,0,0);
     if (i=1) or (i=7) then  ACanvas.pen.Color:=RGBToColor(0,0,255);
    ACanvas.MoveTo(ARect.Left+Round((ARect.Right-ARect.Left)*0.1),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8)));
    ACanvas.LineTo(ARect.Left+Round((ARect.Right-ARect.Left)*0.9),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8)));
    //绘制标签
    ACanvas.Brush.Color:=RGBToColor(255,255,255);
    tempstr:=FormatFloat('0.000',(ALevel.AVG+4*ALevel.SD-i*ALevel.SD));
    ACanvas.TextOut(ARect.Left+Round((ARect.Right-ARect.Left)*0.1)-ACanvas.TextWidth('8')*length(tempstr),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8))-ACanvas.TextHeight('8') div 2,tempstr);

    if i=2 then ACanvas.TextOut(ARect.Left+Round((ARect.Right-ARect.Left)*0.9),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8))-ACanvas.TextHeight('8') div 2,'+2sd');
    if i=6 then ACanvas.TextOut(ARect.Left+Round((ARect.Right-ARect.Left)*0.9),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8))-ACanvas.TextHeight('8') div 2,'-2sd');
    if i=4 then ACanvas.TextOut(ARect.Left+Round((ARect.Right-ARect.Left)*0.9),ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+i*0.85/8))-ACanvas.TextHeight('8') div 2,'AVG');
  end;


  //绘制纵向坐标
  ACanvas.pen.Color:=RGBToColor(200,200,200);
  for i := 0 to ALevel.PointCount+1  do
  begin
    ACanvas.MoveTo(ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+i*0.8/(ALevel.PointCount+1))),ARect.Top+Round((ARect.Bottom-ARect.Top)*0.05));
    ACanvas.LineTo(ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+i*0.8/(ALevel.PointCount+1))),ARect.Top+Round((ARect.Bottom-ARect.Top)*0.9));
    if i< ALevel.PointCount  then
    begin
      ACanvas.Font.Size:=12;
      ACanvas.Font.Size:=Round(((ARect.Right-ARect.Left)*0.8/(ALevel.PointCount+1))*12/(ACanvas.TextWidth('8')*2));
      if ACanvas.TextHeight('8')>Round((ARect.Bottom-ARect.Top)*0.048) then
      begin
        tempInt:=ACanvas.Font.Size;
        ACanvas.Font.Size:=Round((ARect.Bottom-ARect.Top)*0.048*tempInt/ ACanvas.TextHeight('8'));
      end;
      if (i mod 2)=0 then
      begin
        tempStr:=FormatDateTime('d',ALevel.Points[i].Time);
        tempX:=ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+(i+1)*0.8/(ALevel.PointCount+1)))-ACanvas.TextWidth('8')*length(tempStr) div 2;
        tempY:=ARect.Top+Round((ARect.Bottom-ARect.Top)*0.9)+1;
        ACanvas.TextOut(tempX,tempY,tempStr);
      end
      else
      begin
        tempStr:=FormatDateTime('d',ALevel.Points[i].Time);
        tempX:= ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+(i+1)*0.8/(ALevel.PointCount+1)))-ACanvas.TextWidth('8')*length(tempStr) div 2;;
        tempY:= ARect.Top+Round((ARect.Bottom-ARect.Top)*0.95);
        ACanvas.TextOut(tempX,tempY,tempStr);
      end;
    end;
  end;



end;

procedure TLJChart.DrawLine(ACanvas:TCanvas;ALevel:QCLevel;ARect:TRect);
var
  aryPoints:array of TPoint;
  i:integer;
  AVGY:Integer;
  pointR:integer;
begin
  //计算X坐标
  setLength(aryPoints,ALevel.PointCount);
  for i:=0 to ALevel.PointCount-1 do
  begin
    aryPoints[i].x:=ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+(i+1)*0.8/(ALevel.PointCount+1)));
  end;

  //计算Y坐标
  for i:=0 to ALevel.PointCount-1 do
  begin
    if  (ALevel.Points[i].Value>ALevel.AVG+ALevel.SD*4) or (ALevel.Points[i].Value<ALevel.AVG-ALevel.SD*4) then
    begin
      if   (ALevel.Points[i].Value>ALevel.AVG+ALevel.SD*4) then  aryPoints[i].y:=Round((ARect.Bottom-ARect.Top)*0.05);
      if  (ALevel.Points[i].Value<ALevel.AVG-ALevel.SD*4) then aryPoints[i].y:=Round((ARect.Bottom-ARect.Top)*0.9);
    end
    else
    begin
      AVGY:=ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+0.85/2));
      aryPoints[i].y:=AVGY-Round(((ALevel.Points[i].Value-ALevel.AVG)/ALevel.SD)*(ARect.Bottom-ARect.Top)*0.85/8);
    end;
  end;
  //绘制点
  pointR:=Round((ARect.Right-ARect.Left)/(ALevel.PointCount*8));
  for i:=0 to ALevel.PointCount-1 do
  begin
    ACanvas.Pen.Color:=RGBToColor(255,0,0);
    ACanvas.Brush.Color:= RGBToColor(0,0,0);
    ACanvas.Ellipse(aryPoints[i].x-pointR,aryPoints[i].y-pointR,aryPoints[i].x+pointR,aryPoints[i].y+pointR);

  end;
  //连接点

  for i:=0 to ALevel.PointCount-1 do
  begin
    if i=0 then ACanvas.MoveTo(aryPoints[i].x,aryPoints[i].y);
    if i<ALevel.PointCount-1 then ACanvas.LineTo(aryPoints[i+1].x,aryPoints[i+1].y);
  end;
end;
procedure TLJChart.AddGraphPoint(ALevel:QCLevel;ARect:TRect);
var
  i:integer;
  iCount:integer;
  AVGY:Integer;
  pointR:integer;
begin
  iCount:=length(FPoints);

  SetLength(FPoints,iCount+ALevel.PointCount);
  //计算X坐标
  for i:=0 to ALevel.PointCount-1 do
  begin
    FPoints[iCount+i].x:=ARect.Left+Round((ARect.Right-ARect.Left)*(0.1+(i+1)*0.8/(ALevel.PointCount+1)));
  end;
  //showmessage(inttostr(FPoints[iCount-1].x));

  //计算Y坐标
  for i:=0 to ALevel.PointCount-1 do
  begin
    if  (ALevel.Points[i].Value>ALevel.AVG+ALevel.SD*4) or (ALevel.Points[i].Value<ALevel.AVG-ALevel.SD*4) then
    begin
      if   (ALevel.Points[i].Value>ALevel.AVG+ALevel.SD*4) then  FPoints[iCount+i].y:=Round((ARect.Bottom-ARect.Top)*0.05);
      if  (ALevel.Points[i].Value<ALevel.AVG-ALevel.SD*4) then FPoints[iCount+i].y:=Round((ARect.Bottom-ARect.Top)*0.9);
    end
    else
    begin
      AVGY:=ARect.Top+Round((ARect.Bottom-ARect.Top)*(0.05+0.85/2));
      FPoints[iCount+i].y:=AVGY-Round(((ALevel.Points[i].Value-ALevel.AVG)/ALevel.SD)*(ARect.Bottom-ARect.Top)*0.85/8);
    end;
  end;

  pointR:=Round((ARect.Right-ARect.Left)/(ALevel.PointCount*8));

  //赋予其它值
  for i:=0 to ALevel.PointCount-1 do
  begin
    FPoints[iCount+i].r:=pointR;
    FPoints[iCount+i].value:=ALevel.Points[i].Value;
    FPoints[iCount+i].RSD:= RoundTo((ALevel.Points[i].Value-ALevel.AVG)/ALevel.SD,-2);
    FPoints[iCount+i].AVG:=ALevel.AVG;
  end;

end;

procedure TLJChart.Print(Index:integer);
var
  rect:TRect;
begin
  Printer.BeginDoc;
  //DrawChart(Printer.Canvas);
  //Printer.Canvas.TextOut(0,0,'dafdsa');
  rect.Left:=0;
  rect.Top:=0;
  rect.Right:=Printer.PageWidth;
  rect.Bottom:=Printer.PageHeight;
  DrawChart(Printer.Canvas,FQCData,rect);
  //DrawAxis(Printer.Canvas,FQCData.Levels[0],rect);
  Printer.EndDoc;
end;



end.

吵吵微信朋友圈,请付款实名加入:

吵吵 吵吵

3条回应:“L-J质控图LJChart组件for Lazarus”

  1. aecth说道:

    质控我不太懂。有个小问题,高值质控和低值质控,不是分开画的么?是要画在一张上么?一天做多次是指什么呢?我们以前如果遇到失控的情况,当天会保留两个结果,一次是失控,另一次是在控的,不管做多少次,保留第一次的失控值以及最后一次的在控值。如果全部在控,就保留最后一次的结果。画图的时候,会用红色在质控图上把失控的给标出来,连线的时候,是连在控值。

    • 吵吵博客说道:

      应该是分开画的,划在一起也可以,叫z分数图。我说的是质控频率,比如血常规一天做三次质控,你只算最后一次的话,就没有纳入前两次的质控了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注