Programmer » NotifyEdit

NotifyEdit is a validating edit control for Borland C++Builder, written by Lucian Wischik. You're welcome to use it in whatever way you wish.


This component was originally written as part of my own application. It has not been tested as a robust stand-alone component. Because of this I have presented the component here only as source code. You are expected to know enough about programming BCB components to make sense of it. I have cut and pasted this code out of my current application, and some errors might have crept in during the process. Sorry!

There is one special consideration. There's a function NotifyEditIsDialogOkay(.) which must be called before mrOk is set. The code below suggests where to call it. (The role of this function is to stop the user from clicking Ok if there are still invalid things on the form.)


New Properties

ColorInvalid property

This property says what color the control should go when it has an invalid value inside. It is clRed by default.

InvalidityAllowed property

NotifyImmediacy property

OnValidateOrChange event

This event is where you can do your validation. It is called when the contents of the edit are changed. The arguments are AnsiString &Text and bool &Valid. You can return true or false to say whether you consider this text to be valid. You can also take this opportunity to transform the text that has been entered.


Notes on implementation

This component has methods and events for a 'validating edit control'. It calls an event OnValidateOrChange to check if the value is okay. When the value is invalid, this is indicated by coloring the edit box red. It can be set in the mode Invalidity-Allowed-Never, in which you're not even allowed to type in an invalid value. Or Invalidity-Allowed-Inside-Only, in which invalid values are allowed but you can never leave focus if the value is invalid. Or Invalidity-Allowed-Always, in which you can leave focus but you can't OK the dialog.

Actually, that last point requires some code on the part of the user. Before you attempt to set ModalResult=mrOk in any of your code, you must first check with the global function NotifyEditIsDialogOkay, and abort your attempt if it returns false. Or alternatively override your WndProc and make the call if ModalResult happens to be mrOk. (Unfortunately this could not be done automatically by subclassing: read the implementation notes to see why)

The component does trap the Enter and Escape keys when it has focus. This is only useful if you set it to Invalidity-Allowed-Inside-Only. It will disallow the RETURN key processing if invalid; and if it is invalid then pressing ESCAPE will revert it back. You can disable this feature with the HandleDialogKeys property.

Valid is an internal flag which we update every single keypress, in the call UpdateValidity, which itself calls the function ValidateOrChange, which calls the event of the same name.

Incidentally, the control is deemed to be valid in its initial state. That's so that you can always close a dialog box that you just opened.

We keep track of the last valid value to have been there. This is useful in case we need to revert to it. We update this internal value every time some text is passed by the validator.

We also keep track with an FDirty flag for whether the contents have changed since the last time we notified a change. This is so that, for instance, merely loosing focus (eg. tabbing through all the controls in order) doesn't result in spurious change-notifications. I believe that setting the Text property is always vectored through EN_UPDATE, so EN_UPDATE is where we will set the dirty flag

InvalidityAllowed might be iaNever, iaInsideOnly or iaAlways. If it's never then invalid changes cause immediate reverting. That's the only case where we revert (apart from the escape key when invalid). There's one extra: iaInsideMainly is like iaInsideOnly, but lets focus be lost to a mrCancel. Devious users could focus on the mrCancel without actually clicking it. If that is going to be a problem, use iaInsideOnly. If it isn't, then iaInsideMainly is the most natural user interface.

NotifyImmediacy might be niImmediately or niOnKillFocus. Immediately means that the ContentsChanged procecdure is called immediately each change is made. OnKillFocus means that notifies only happen when the control looses focus or this DialogOkay thing happens.

Every single change that happens, every single character typed, results in a call to UpdateValidity. This first calls ValidateOrChange to get the answer and (optionally) change the text guarded with an Ingore flag, then changes the color.

Also at that change, if it was invalid and iaNever, then we revert. If it was valid and niImmediate, then we notify a change. Or if it was valid but we're not the focus control (eg. an up-down buddy set our text): the fact that we're not focused will indicate such a scenario

Leaving is when it looses focus via CM_EXIT, or when return is pressed, or when someone calls IsDialogOkay. If iaInsideOnly and its invalid then we disallow the leave (ie. disallow the loose-focus, or disallow the return, or disallow the IsDialogOkay). If iaAlways and its invalid then we allow leave but disallow return and IsDialogOkay. If the thing is valid on a leave, we perhaps notify a change

I've chosen to leave everything protected. This is for you to inherit your own components from.

I've tested it against user input. I don't know how it will behave if you choose to set its Text property programatically. If your own implementation of the OnValidateOrChange event changes, you should call UpdateValidity().

How and when we actually perform the validation

CM_EXIT is a VCL message. It is called when someone does OtherControl->SetFocus(), (in which case we set Msg.Result=0 so it can't happen) and when the user does a tab or elsewhere-click when Windows sends a WM_KILLFOCUS (in which case we SetFocus() to bring it back). ie. we have to do both of them in response to the message.

We'd like a general way to check for someone setting ModalResult (eg. from an OK button) and intercepting that. There are two possible routes. WM_COMMAND -> CNCommand -> Click, which sets ModalResult. Or CM_DIALOGKEY (if the user pressed Enter). Onfortunately, one or both (I can't remember) is done with a call to Dispatch, which goes directly to the VCL's notion of WndProc and not to GWL_WNDPROC. Therefore we can't subclass it. Instead, you might call NotifyEditIsDialogOkay before setting your ModalResult to mrOk. Alternatively, you could override your WndProc (which Dispatch does call) and have it first call the ancestor method, and then if ModalResult has been set to mrOk then do the check. You could check for it all the time, or just on CM_DIALOGKEY or WM_COMMAND.

There is one special case. If you have a control which allows invalidity only inside, then the only way you could have clicked on the default OK button would have been by pressing Enter. It would be very easy of us to trap this special case and make it gobble up the message. And we do! Thus, if none of your controls allow invalidity outside, then you don't need the above message. There is a problem that you might have wanted to use the enter key (perhaps in the guise of a non-mrOk default button) to do some sort of processing. Tough!

To implement the iaInsideMainly option, we needed to know where the focus was being lost to. So we override WM_KILLFOCUS and take a note of the focus destination. This is carried over into our CM_EXIT processing where the action we take depends on whether we were loosing focus to a mrCancel button or not.

Another special case is pressing Escape inside an invalid control, whereupon it reverts. We trap this case again in WM_KEYDOWN looking for VK_ESCAPE.


IsDialogOkay

This function must be called before setting ModalResult to mrOk. it checks that all the NotifyEdit controls on the form are valid and, if any of them aren't, sets focus to that control and return false (indicating that you should not proceed with your mrOk). Unfortunately it's impossible to trap this in the regular ShowModal routine.

bool __fastcall PACKAGE NotifyEditIsDialogOkay(TCustomForm *f)
{ for (int i=0; i<f->ComponentCount; i++)
  { TNotifyEdit *ne=dynamic_cast<TNotifyEdit*>(f->Components[i]);
    if (ne!=NULL)
    { bool res=ne->IsDialogOkay();
      if (!res) return false;
    }
  }
  return true;
}

This is the first way you might choose to use IsDialogOkay: setting mrOk dynamically (instead of with the button's ModalResult property)

void __fastcall TForm1::bOkClick(TObject *Sender)
{ if (!NotifyEditIsDialogOkay(this)) return;
  ModalResult = mrOk;
}

This is the second way you might choose to use it. This way you can keep your buttons' ModalResult properties as they were. However, you'll probably want some sort of flag FShowModalEx to say when the form is being shown modally.

void __fastcall TForm1::WndProc(TMessage &Message)
{ TForm::WndProc(Message);
  if (FShowModalEx && (Message.Msg==WM_COMMAND || Message.Msg==CM_DIALOGKEY) && ModalResult==mrOk)
  { ModalResult=mrNone; bool res=NotifyEditIsDialogOkay(this); if (res) ModalResult=mrOk;
  }
}

Header file 'notify_edit.h'

enum TNotifyEditInvalidityAllowed {iaNever, iaInsideOnly, iaInsideMainly, iaAlways};
enum TNotifyEditNotifyImmediacy {niImmediate,niOnKillFocus};
typedef void __fastcall (__closure *TNotifyEditValidateEvent)(System::TObject *Sender, AnsiString &Text,bool &Valid);

class PACKAGE TCustomNotifyEdit : public TCustomEdit
{
public:
  __fastcall TCustomNotifyEdit(TComponent *Owner);
  __property bool Valid = {read=FValid};
  bool __fastcall IsDialogOkay();
protected:
  __property bool HandleDialogKeys = {read=FHandleDialogKeys,write=FHandleDialogKeys};
  __property TNotifyEditInvalidityAllowed InvalidityAllowed = {read=FInvalidityAllowed,write=SetInvalidityAllowed,default=iaInsideMainly};
  __property TNotifyEditNotifyImmediacy NotifyImmediacy = {read=FNotifyImmediacy,write=SetNotifyImmediacy,default=niImmediate};
  __property TNotifyEditValidateEvent OnValidateOrChange = {read=FOnValidateOrChange,write=FOnValidateOrChange,default=NULL};
  __property TNotifyEvent OnChange = {read=FOnChange,write=FOnChange,default=NULL};
  __property TColor Color = {read=FColor,write=SetColor,default=clWindow};
  __property TColor ColorInvalid = {read=FColorInvalid,write=SetColorInvalid,default=clRed};
  //
  TNotifyEditInvalidityAllowed FInvalidityAllowed; virtual void __fastcall SetInvalidityAllowed(TNotifyEditInvalidityAllowed na);
  TNotifyEditNotifyImmediacy FNotifyImmediacy; virtual void __fastcall SetNotifyImmediacy(TNotifyEditNotifyImmediacy ni);
  bool FValid; virtual void __fastcall UpdateValidity();
  TNotifyEditValidateEvent FOnValidateOrChange; virtual bool __fastcall ValidateOrChange(AnsiString *s);
  bool FUnhindered; bool FDirty; AnsiString FRevertText;
  virtual void __fastcall WndProc(TMessage &msg);
  virtual void __fastcall IndicateValidity();
  TNotifyEvent FOnChange; virtual void __fastcall ContentsChanged();
  bool FHandleDialogKeys;
  TColor FColor; virtual void __fastcall SetColor(TColor c);
  TColor FColorInvalid; virtual void __fastcall SetColorInvalid(TColor c);
  bool FLosingFocusToCancel;
};



// NOTIFYEDIT. 
class PACKAGE TNotifyEdit : public TCustomNotifyEdit
{
public:
  __fastcall TNotifyEdit(TComponent *Owner);
__published:
  __property InvalidityAllowed;
  __property NotifyImmediacy;
  __property OnValidateOrChange;
  __property OnChange;
  __property Color;
  __property ColorInvalid;
  //
  __property AutoSelect;
  __property AutoSize;
  __property BorderStyle;
  __property CharCase;
  __property Ctl3D;
  __property DragCursor;
  __property DragMode;
  __property Enabled;
  __property Font;
  __property HideSelection;
  __property ImeMode;
  __property ImeName;
  __property MaxLength;
  __property OEMConvert;
  __property ParentColor;
  __property ParentCtl3D;
  __property ParentFont;
  __property ParentShowHint;
  __property PasswordChar;
  __property PopupMenu;
  __property ReadOnly;
  __property ShowHint;
  __property TabOrder;
  __property TabStop;
  __property Visible;
  __property OnClick;
  __property OnDblClick;
  __property OnDragDrop;
  __property OnDragOver;
  __property OnEndDrag;
  __property OnEnter;
  __property OnExit;
  __property OnKeyDown;
  __property OnKeyPress;
  __property OnKeyUp;
  __property OnMouseDown;
  __property OnMouseMove;
  __property OnMouseUp;
  __property OnStartDrag;
};

Source code 'notify_edit.cpp'

__fastcall TCustomNotifyEdit::TCustomNotifyEdit(TComponent *Owner) : TCustomEdit(Owner)
{ FInvalidityAllowed=iaInsideMainly;
  FNotifyImmediacy=niImmediate;
  FUnhindered=false;
  FValid=true; // it'd be crazy if it wasn't! might mean you couldn't close a dialog box that had been opened
  FRevertText=Text;
  FDirty=false;
  FOnValidateOrChange=NULL;
  FOnChange=NULL;
  FHandleDialogKeys=false;
  FColor=clWindow; FColorInvalid=clRed;
  FLosingFocusToCancel=false;
}
void __fastcall TCustomNotifyEdit::SetInvalidityAllowed(TCustomNotifyEditInvalidityAllowed na)
{ if (FInvalidityAllowed==na) return;
  FInvalidityAllowed=na;
}
void __fastcall TCustomNotifyEdit::SetNotifyImmediacy(TCustomNotifyEditNotifyImmediacy ni)
{ if (FNotifyImmediacy==ni) return;
  FNotifyImmediacy=ni;
  if ((FNotifyImmediacy==niImmediate && FDirty) || (!Focused() && FDirty)) ContentsChanged();
}
bool __fastcall TCustomNotifyEdit::IsDialogOkay()
{ if (Valid)
  { if (FDirty) ContentsChanged();
    return true;
  }
  SetFocus(); return false;
}
void __fastcall TCustomNotifyEdit::UpdateValidity()
{ AnsiString s=Text;
  bool okay=ValidateOrChange(&s);
  FValid=okay;
  if (okay)
  { FDirty=true; FRevertText=Text; IndicateValidity();
  }
  else
  { if (FInvalidityAllowed==iaNever)
    { int i=SelStart; int l=SelLength;
      bool uold=FUnhindered; FUnhindered=true; Text=FRevertText; SelStart=i; SelLength=l; FUnhindered=uold;
      return;
    }
    IndicateValidity();
  }
}
void __fastcall TCustomNotifyEdit::ContentsChanged()
{ FDirty=false;
  if (FOnChange!=NULL) FOnChange(this);

}
bool __fastcall TCustomNotifyEdit::ValidateOrChange(AnsiString *s)
{ bool res=true;
  if (FOnValidateOrChange!=NULL) FOnValidateOrChange(this,*s,res);
  return res;
}
void __fastcall TCustomNotifyEdit::IndicateValidity()
{ if (FValid) TCustomEdit::Color=FColor;
  else TCustomEdit::Color=FColorInvalid;
}
void __fastcall TCustomNotifyEdit::SetColor(TColor c)
{ if (c==FColor) return; FColor=c; IndicateValidity();
}
void __fastcall TCustomNotifyEdit::SetColorInvalid(TColor c)
{ if (c==FColorInvalid) return; FColorInvalid=c; IndicateValidity();
}
void __fastcall TCustomNotifyEdit::WndProc(TMessage &msg)
{ if (ComponentState.Contains(csDesigning)) {TCustomEdit::WndProc(msg);return;}
  if (msg.Msg==CN_COMMAND && HIWORD(msg.WParam)==EN_UPDATE)
  { UpdateValidity();
    if (FDirty && (FNotifyImmediacy==niImmediate || !Focused())) ContentsChanged();
  }
  if (msg.Msg==WM_KILLFOCUS)
  { TButton *btn=dynamic_cast<TButton*>(FindControl((HWND)msg.WParam));
    FLosingFocusToCancel = (btn!=NULL && btn->ModalResult==mrCancel);
  }
  if (msg.Msg==CM_EXIT)
  { bool WasLosingFocusToCancel=FLosingFocusToCancel;
    FLosingFocusToCancel=false;
    bool uold=FUnhindered; FUnhindered=true;
    TCustomEdit::WndProc(msg);
    if (!Valid && FInvalidityAllowed!=iaAlways && !(FInvalidityAllowed==iaInsideMainly && WasLosingFocusToCancel))
    { msg.Result=0;SetFocus();
    }
    if (Valid && FDirty) ContentsChanged();
    FUnhindered=uold;
    return;
  }
  if (msg.Msg==CN_KEYDOWN && HandleDialogKeys)
  { if (msg.WParam==VK_RETURN)
    { bool okay=IsDialogOkay();
      if (!okay)
      { msg.WParam=0;
      }
      else
      { if (FDirty) ContentsChanged();
      }
    }
    if (msg.WParam==VK_ESCAPE && !Valid)
    { bool uold=FUnhindered; FUnhindered=true; Text=FRevertText; FValid=true; IndicateValidity();
      SelStart=0; SelLength=FRevertText.Length(); FUnhindered=uold;
      msg.WParam=0;
    }
  }
  TCustomEdit::WndProc(msg);
}




__fastcall TNotifyEdit::TNotifyEdit(TComponent *Owner) : TCustomNotifyEdit(Owner)
{ 
}
add comment  edit commentPlease add comments here