Right now i have a function that runs a query, using ADO, and returns a recordset:
Recordset Execute(Connection connection, String commandText)
{
//[pseudo-code]
Recordset rs = new Recordset();
rs.CursorLocation = adUseClient;
rs.CursorType = adOpenForwardOnly;
rs.Open(commandText, connection,
adOpenForwardOnly, //CursorType; the default
adLockReadOnly, //LockType
adCmdText);
return rs;
}
And this is fine. It runs synchronously, and returns the recordset of a query.
Now i want a similar version, that shows a ProgressDialog, providing the user with the ability to cancel a long-running query:
Recordset Execute(HWND parenthWnd, String caption, Connection connection, String commandText)
{
//[pseudo-code]
//Construct a progressDialog and show it
IProgressDialog pd = new ProgressDialog();
pd.SetTitle(caption); //e.g. "Annual Funding Report"
pd.SetCancelMsg("Please wait while the operation is cancelled");
pd.StartProgressDialog(parenthWnd, null, PROGDLG_MODAL | PROGDLG_NOTIME | PROGDLG_NOMINIMIZE, null);
pd.SetLine(1, "Querying server", False, null);
try
{
//Query the server
Recordset rs = new Recordset();
rs.Open(commandText, connection,
adOpenForwardOnly, //CursorType
adLockReadOnly, //LockType
adCmdText | adAsyncExecute);
while (rs.State and (adStateConnecting+adStateExecuting+adStateFetching) <> 0)
{
if pd.HasUserCancelled()
throw new EUserCancelledOperationException();
Sleep(100);
};
finally
{
//Hide and destroy the progress dialog
pd.StopProgressDialog();
pd = null;
}
//Now we have our results for the client
return rs;
}
The way to check if the user has cancelled the operation is to periodically ask the progress dialog if the user as pressed the cancel button:
pd.HasUserCancelled(); //returns true if user has clicked Cancel
Now i'm faced with how to periodically check if the user has cancelled (and to know if the query has completed, or an error has happened), and to be a good programmer and do it without polling.
The only way to know that an error has happened is to have a handler on the Recordset's FetchCompleteEvent:
pError
An Error object. It describes the error that occurred if the value of adStatus is adStatusErrorsOccurred; otherwise it is not set.
adStatus
An EventStatusEnum status value. When this event is called, this parameter is set to adStatusOK if the operation that caused the event was successfull, or to adStatusErrorsOccurred if the operation failed.
Before this event returns, set this parameter to adStatusUnwantedEvent to prevent subsequent notifications.
pRecordset
A Recordset object. The object for which the records were retrieved.
So this would imply that i'm going to have to have my function construct a helper object, so i can have a FetchComplete handler. But then i have to prevent my synchronous function from returning right away. And then we get into MsgWaitForSingleObject, which is notoriously difficult to use correctly.
So i'm asking for assistance, or canned code.
i should be more explicit: i'm looking for an implementation of function with this method signature:
Recordset ExecuteWithCancelOption(Connection connection, String commandText)
that shows a dialog with a cancel button on it.
The challenge is that the function must now create whatever is required to achieve that. If that involves a hidden form, that has a timer on it, etc - okay.
But i'm looking for a synchronous function that displays a Cancel button.
And the function is going to be a near (or exact) drop-in replacement for
Recordset Execute(Connection connection, String commandText)
Given practical considerations on Windows, i would need to supply the function with a parent window handle that it will parent its dialog to:
Recordset ExecuteWithCancelOption(HWND parentHwnd, Connection connection, String commandText)
And given that this is going to be a reusable function, i'll let the caller provide the text that will be displayed:
Recordset ExecuteWithCancelOption(HWND parenthWnd, String caption, Connection connection, String commandText)
And given that these are both class functions in my TADOHelper class, i can give them the same name, and have them be overloads of one another:
Recordset Execute(HWND parenthWnd, String caption, Connection connection, String commandText)
i would think in languages other than Delphi, anonymous delegates are helpful. But i'm still terrified of having to deal with MsgWaitForMultipleObjects.
Progress informations and gracefully cancelling a query may not be available in every database engine. They need database support, both on the server and the client side. For example Oracle allows cancelling a query, yet has no "on progress" information but reading the V$SESSION_LONGOPS view. Sure, you can kill the session, but it will rollback the whole of it, not just cancel a give query execution.
Usually if the database supports this kind of features, the query is run in a separate thread that will wait for the result. That way the main thread can still get user input or read and display progress information (unless returned in some kind of callback). If the user cancels the query then the appropriate call is issued to stop the operation, allowing the query thread to return, usually the thread will receive a status code that will tell what's happened.
Be aware of how ADO implements async operations: http://msdn.microsoft.com/en-us/library/ms681467(VS.85).aspx
There's also a FetchProgress() event that could help you if you don't want to go the thread way (and even then to cancel a the query, if possible)
In order to make GUI to respond to button clicks you should return control to the message loop of the window. While loop while (rs.State and (adStateConnecting+adStateExecuting+adStateFetching) <> 0) does not return control back to the message loop thus blocking GUI.
Below is an excerpt from a working Delphi code that uses asyncronous ADO queries. This code does not allow for non-modal fetching of data, but ensures that the main form is repainted during data fetch and also allows cancelling the query.
Asynchronous execution and fetching is achieved by setting:
FOpeningDataSet.ExecuteOptions := [eoAsyncExecute, eoAsyncFetchNonBlocking];
execution of query is cancelled by calling
DataSet.Recordset.Cancel;
in FetchProgress event.
Any TADODataSet shall be opened via the method:
OpenDataSetInBackground(DataSourceData.DataSet as TADODataSet);
Supporting code in the main form:
procedure TOperatorForm.OpenDataSetInBackground(DataSet: TADODataSet);
begin
if DataSet.Active then Exit;
FOpeningDataSet := DataSet;
if not FAsyncDataFetch then
begin
FOpeningDataSet.Open;
Exit;
end;
FFetchCancel := False;
FExecuteOptions := FOpeningDataSet.ExecuteOptions;
FFetchProgress := FOpeningDataSet.OnFetchProgress;
FFetchComplete := FOpeningDataSet.OnFetchComplete;
FRecordsetCreate := FOpeningDataSet.OnRecordsetCreate;
FAfterScroll := FOpeningDataSet.AfterScroll;
FOpeningDataSet.ExecuteOptions := [eoAsyncExecute, eoAsyncFetchNonBlocking];
FOpeningDataSet.OnFetchProgress := DataSetFetchProgress;
FOpeningDataSet.OnFetchComplete := DataSetFetchComplete;
FOpeningDataSet.OnRecordsetCreate := DataSetRecordsetCreate;
FOpeningDataSet.AfterScroll := DataSetAfterScroll;
FOpeningDataSet.CursorLocation := clUseClient;
FOpeningDataSet.DisableControls;
try
DataSetProgressForm.Left := Left + (Width - DataSetProgressForm.Width) div 2;
DataSetProgressForm.Top := Top + (Height - DataSetProgressForm.Height) div 2;
DataSetProgressForm.cxButton1.OnClick := DataSetProgressClick;
DataSetProgressForm.cxButton1.Visible := FShowProgressCancelButton;
FOpeningDataSet.Open;
DataSetProgressForm.ShowModal;
finally
FOpeningDataSet.EnableControls;
FOpeningDataSet.ExecuteOptions := FExecuteOptions;
FOpeningDataSet.OnFetchProgress := FFetchProgress;
FOpeningDataSet.OnFetchComplete := FFetchComplete;
FOpeningDataSet.OnRecordsetCreate := FRecordsetCreate;
FOpeningDataSet.AfterScroll := FAfterScroll;
end;
end;
procedure TOperatorForm.DataSetProgressClick(Sender: TObject);
begin
FFetchCancel := True;
end;
procedure TOperatorForm.DataSetFetchProgress(DataSet: TCustomADODataSet; Progress, MaxProgress: Integer; var EventStatus: TEventStatus);
begin
if FFetchCancel then
DataSet.Recordset.Cancel;
end;
procedure TOperatorForm.DataSetFetchComplete(DataSet: TCustomADODataSet; const Error: Error; var EventStatus: TEventStatus);
begin
PostMessage(DataSetProgressForm.Handle, WM_CLOSE, 0, 0);
MessageBeep(MB_ICONEXCLAMATION);
end;
procedure TOperatorForm.DataSetFetchComplete(DataSet: TCustomADODataSet; const Error: Error; var EventStatus: TEventStatus);
begin
PostMessage(DataSetProgressForm.Handle, WM_CLOSE, 0, 0);
MessageBeep(MB_ICONEXCLAMATION);
end;
procedure TOperatorForm.DataSetRecordsetCreate(DataSet: TCustomADODataSet; const Recordset: _Recordset);
begin
if Assigned(FRecordsetCreate) then FRecordsetCreate(DataSet, Recordset);
end;
procedure TOperatorForm.DataSetAfterScroll(DataSet: TDataSet);
begin
// From TBetterADODataSet 4.04
// Ole Willy Tuv's fix 03-10-00 for missing first record
with TADODataSet(DataSet) do
begin
if (eoAsyncFetchNonBlocking in ExecuteOptions) and
(Bof or Eof) and
(CursorLocation = clUseClient) and
(stFetching in RecordSetState) then
begin
if Recordset.RecordCount > 0 then
if Bof then
Recordset.MoveFirst
else if Eof then
Recordset.MoveLast;
CursorPosChanged;
Resync([]);
end;
end;
if Assigned(FAfterScroll) then
FAfterScroll(DataSet);
end;
Progress form:
unit uDataSetProgressForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, ExtCtrls, StdCtrls;
type
TDataSetProgressForm = class(TForm)
AnimateProgress: TAnimate;
Label1: TLabel;
Bevel1: TBevel;
Bevel2: TBevel;
Button1: TButton;
Shape1: TShape;
procedure FormCreate(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure FormHide(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
DataSetProgressForm: TDataSetProgressForm;
implementation
{$R *.dfm}
{$R servertimeout.res} // contains IDR_SERVAVI animation resource
procedure TDataSetProgressForm.FormCreate(Sender: TObject);
begin
AnimateProgress.ResName := 'IDR_SERVAVI';
end;
procedure TDataSetProgressForm.FormShow(Sender: TObject);
begin
AnimateProgress.Active := True;
end;
procedure TDataSetProgressForm.FormHide(Sender: TObject);
begin
AnimateProgress.Active := False;
end;
end.
and dfm
object DataSetProgressForm: TDataSetProgressForm
Left = 590
Top = 497
BorderStyle = bsNone
ClientHeight = 104
ClientWidth = 205
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
FormStyle = fsStayOnTop
OldCreateOrder = False
Position = poDefaultSizeOnly
OnCreate = FormCreate
OnHide = FormHide
OnShow = FormShow
DesignSize = (
205
104)
PixelsPerInch = 96
TextHeight = 13
object Bevel1: TBevel
Left = 0
Top = 0
Width = 205
Height = 104
Align = alClient
Style = bsRaised
end
object Bevel2: TBevel
Left = 12
Top = 12
Width = 181
Height = 80
Anchors = [akLeft, akTop, akRight, akBottom]
end
object Shape1: TShape
Left = 1
Top = 1
Width = 203
Height = 102
Anchors = [akLeft, akTop, akRight, akBottom]
Brush.Style = bsClear
Pen.Color = clWindowFrame
end
object AnimateProgress: TAnimate
Left = 25
Top = 23
Width = 32
Height = 32
end
object Label1: TLabel
Left = 70
Top = 31
Width = 106
Height = 17
Hint = 'Selecting data...'
Caption = 'Selecting data...'
TabOrder = 1
end
object Button1: TButton
Left = 63
Top = 64
Width = 80
Height = 23
Caption = 'Cancel'
Default = True
TabOrder = 2
end
end
If it is Delphi you can drop a TTimer component in and use that to check if HasUserCancelled value is True. I don't have Delphi in front of me so I'd have to post an example later.
Edit:
Here's an example of a TTimer OnTimer event that checks the current time and the lastactivity time to decide what to do with the forms if the program has been left "Up":
procedure TForm_Main.Timer1Timer(Sender: TObject);
begin
// return to opening screen if no activity for a while:
if Now - LastActivity > TimeOut
then
begin
Form_Select.SBtn_Defendant.Down:= False;
Form_Select.SBtn_Officer.Down:= False;
Form_Select.SBtn_Attorney.Down:= False;
Form_Main.ModalResult:= mrCancel;
Exit;
end;
Form_Main.Caption:= FormatDateTime('dddd mmmm d, yyyy h:nn:ss AM/PM', Now);
end;
Related
I have a procedure which needs to read data from an ini file with the following format:
'Prices', [integer], [data to be read]
The data read consists of two pieces of information split by a '/' symbol. The data is split successfully when I call my procedure below.
I have a TValueListEditor (called ledtPrices) placed on the form and would like to add the values from the ini file to the List Editor. If I call ledtPrices.InsertRow via a button click, the values I enter into to add to the row are added and the list editor is refreshed.
However, when I call the same function from my RefreshPList procedure, the values are not added as new rows (the list editor is blank). I have tested my code with ShowMessage dialogues to ensure each part of the procedure is functioning when it should. My code is as follows:
procedure RefreshPList;
var
l: TValueListEditor;
xFile: TINIFile;
temprow, tl, tp: string;
tempr: TStringList;
i: integer;
begin
i := 0;
l := frmSettings.ledtPrices;
try
tempr := TStringList.Create;
tempr.StrictDelimiter := True;
tempr.Delimiter := '/';
xFile := TIniFIle.Create('C:\MData.ini');
try
temprow := xFile.ReadString('Prices', '0', 'xx');
if temprow = 'xx' then
ShowMessage('no prices saved')
else
begin
repeat
temprow := xFile.ReadString('Prices', IntToStr(i), 'xx');
if temprow <> 'xx' then
begin
tempr.DelimitedText := temprow;
tl := tempr[0];
tp := tempr[1];
l.InsertRow(tl,tp,true);
//ShowMessage(tl);
Inc(i);
end
else
ShowMessage('End of list');
until (temprow = 'xx');
//l.Refresh;
end;
finally
xFile.Free;
end;
LastLine := i;
finally
tempr.Free;
end;
end;
LastLine is a global integer value to be used later. I'm trying to add, remove and edit data within the list editor, without editing the cells directly. The procedure to add new data to the ini file has been written and runs successfully.
UPDATE
I've come to realise that any procedure that I create which tries to edit a components values does not edit the components values. Am I missing something simple here?
For example, I created a memo on the form and created a procedure which adds the contents of an array to the memo.lines. This procedure did not execute when called from a buttonclick. However, if I copy the contents of the procedure directly into the buttonclick and execute it, it works.
The procedures are called from buttonclick commands. The form is created from a mainform. The components all sit within a pagecontrol tabsheet.
A quick test application (XE5, VCL Forms) cannot reproduce the problem.
I start with a new blank application, drop a TValueListEditor and a TButton on the form, and use the Object Inspector to add two key/value combinations:
Key Value
--- -----
A Aaaaaaa
C Ccccccc
In the TButton.OnClick event, I use the following code:
procedure TForm1.Button1Click(Sender: TObject);
var
NewKey, NewValue: string;
begin
NewKey := 'B';
NewValue := 'Bbbbbbb';
ValueListEditor1.InsertRow(NewKey, NewValue, True);
end;
I run the application:
I click Button1, and the code successfully adds the new item at the end (bottom) of the TValueListEditor.
I change the last parameter to InsertRow to False, and it inserts it at the start (top) of the TValueListEditor.
This indicates that either you're not getting the values you expect from your ini file, or the code that inserts the new row isn't executing.
Here's the full code of the test app I created:
Unit1.dfm
object Form4: TForm1
Left = 0
Top = 0
Caption = 'Form4'
ClientHeight = 178
ClientWidth = 447
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object ValueListEditor1: TValueListEditor
Left = 40
Top = 16
Width = 306
Height = 137
Strings.Strings = (
'A=Aaaaaaa'
'C=Ccccccc')
TabOrder = 0
end
object Button1: TButton
Left = 352
Top = 16
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 1
OnClick = Button1Click
end
end
Unit1.pas
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Grids, Vcl.ValEdit;
type
TForm1 = class(TForm)
ValueListEditor1: TValueListEditor;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
NewKey, NewValue: string;
begin
NewKey := 'B';
NewValue := 'Bbbbbbb';
ValueListEditor1.InsertRow(NewKey, NewValue, True);
end;
end.
How can I release a Variable that is being used by the WebService.
I'm using this form:
HttpPrincipal.WSDLLocation: = FrmPrincipal.edtWS.Text;
HttpPrincipal.Service: = 'CADServicesService';
HttpPrincipal.Port := 'CADServices';
Trinity: = HttpPrincipal as CADServices;
At the moment when I will close the Form appears an error, and discovered that when I declare this part:
Trinity: = HttpPrincipal as CADServices;
I think it is getting stuck in memory.
The error is the following:
"Invalid Pointer"
The error happens when you close the form, does not have any event in the OnClose or OnDestroy form.
Descriptions:
Trinity : CADServices,
HttpPrincipal is a THTTPRIO,
CADServices is my Unit containing all procedures / functions from WebService.
Instead of using the designtime component try to create HTTPRIO at runtime:
function GetCadServices(Addr : String): CadServices;
const
defSvc = 'CADServicesService';
defPrt = 'CADServices';
var
RIO: THTTPRIO;
begin
Result := nil;
RIO := THTTPRIO.Create(nil)
try
Result := (RIO as CadServices);
RIO.WSDLLocation := Addr;
RIO.Service := defSvc;
RIO.Port := defPrt;
finally
if (Result = nil) then
RIO.Free;
end;
end;
Usage:
Trinity := GetCadServices(FrmPrincipal.edtWS.Text);
If you imported the WSDL with the WSDL importer this code is automatically generated for you (look in the CadServices1 unit)
CString m_strRemorcaNmb; // value for this string is set before
CString path = "DB\\dataBase";
CDaoDatabase db;
try
{
db.Open(path, 0, 0, "");
CDaoRecordset rs(&db);
rs.Open(AFX_DAO_USE_DEFAULT_TYPE, _T("SELECT Numar_inmatriculare FROM Masini;"), 0);
COleVariant searched(m_strRemorcaNmb);
BOOL bFound = rs.Seek("=",&searched);
}
Here i try to verify if a CString value is contained in my data base (.mdb). When it reaches BOOL bFound = rs.Seek("=",&searched); a debug assertion failed error is thrown. Hitting retry on the dialog box the application triggers a breakpoint in daocore.cpp at this line ASSERT(m_nOpenType == dbOpenTable);.
To use Seek you have to have a table-type recordset. (See MSDN: http://msdn.microsoft.com/en-US/library/k3tkt1zd%28v=vs.80%29.aspx) To get this, you have to specify the type when you open the recordset.
Example:
rs.Open(CDaoRecordSet::dbOpenTable,"table1");
This way of checking, if a specific record exists, is very slow. You suck all the records over the network and then check them. It is way better to modify the filter of the recordset before opening, and then check if any records have been returned.
Method A:
sql = "SELECT count(*) AS xyz FROM table WHERE Field='value'";
rs.Open(CDaoRecordset::dbOpenSnapshot,sql);
COleVariant count = rs.GetFieldValue(0);
Method B for generated recordsets:
rs.m_strFilter.Format("Field = '%s'", value);
rs.Open(CDaoRecordset::dbOpenSnapshot,"table");
if(rs.IsEOF()) // no records returned
{
}
I'm trying to write some Pascal script for a installer I'm making with Inno Setup Compiler 5.5.1. I'm currently trying to add a custom wizard page that executes a command, taking user input from text fields (TEdit components). I defined the NextButtonClick function, and it checks that the Page ID is the custom page I defined and attempts to retrieve the user input from the field. When I get it from the components of the Page's Surface property, it gets returned as a TComponent. To get the next I need to cast it to a TEdit, so I tried casting it and it seems to be returning nil. Besides the scripting for Inno I've been doing for the past few days, I don't have much experience with Pascal, so I could possibly be doing something wrong. But I'd appreciate the help!
Here's the chunk of code giving me an issue for reference (with debugging lines left in):
function NextButtonClick(CurPageID: Integer): Boolean;
var
ResultCode: Integer;
CurrPage: TWizardPage;
Server : TComponent;
Server2: TEdit;
SurfacePage : TNewNotebookPage;
ServerStr : String;
begin
if CurPageID = 100 then
begin
CurrPage := PageFromID(100);
SurfacePage := CurrPage.Surface;
Server := SurfacePage.Controls[0];
Server2 := TEdit(Server); // RETURNS NIL HERE
if Server2 = nil then
MsgBox('', mbInformation, MB_OK);
ServerStr := Server2.Text;
MsgBox(ServerStr, mbInformation, MB_OK);
//ShellExec('', 'sqlcmd', '-S ' + ServerStr + ' -Q ":r setMemUsage.sql"', ExpandConstant('{app}') + '\sql', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
Result := True;
end;
I can't simulate your problem. I've used this minimalistic code:
[Code]
var
CustomPageID: Integer;
procedure InitializeWizard;
var
EditBox: TEdit;
CustomPage: TWizardPage;
begin
CustomPage := CreateCustomPage(wpWelcome, '', '');
CustomPageID := CustomPage.ID;
EditBox := TEdit.Create(WizardForm);
EditBox.Parent := CustomPage.Surface;
end;
procedure CurPageChanged(CurPageID: Integer);
var
EditBox: TEdit;
Component: TComponent;
CustomPage: TWizardPage;
begin
if (CurPageID = CustomPageID) then
begin
CustomPage := PageFromID(CustomPageID);
Component := CustomPage.Surface.Controls[0];
if (Component is TEdit) then
begin
MsgBox('Controls[0] is assigned and is TEdit', mbInformation, MB_OK);
EditBox := TEdit(Component);
EditBox.Text := 'Hi, I''m just a modified edit text!';
end;
end;
end;
I need a .Net's FileSystemWatcher analog in raw C++/WinAPI.
I almost started to code one myself using FindFirstChangeNotification/FindNextChangeNotification, but then it occurred to me that I am probably not the first one who needs this and maybe someone will be willing to share.
Ideally what I need is a class which can be used as follows:
FileWatcher fw;
fw.startWatching("C:\MYDIR", "filename.dat",
FileWatcher::SIZE | FileWatcher::LAST_WRITE,
&myChangeHandler);
...
fw.stopWatching();
Or if it would use somehting like boost::signal it would be even better.
But please, no dependencies other than the Standard Library, boost and raw WinAPI.
Thanks!
What about the ReadDirectoryChangesW function?
http://msdn.microsoft.com/en-us/library/aa365465(VS.85).aspx
It stores notifications in a buffer so you don't miss any changes (unless the buffer overflows)
2021 answer:
A forked version of the repo listed below that is actively maintained: https://github.com/SpartanJ/efsw
Old answer:
This is a cross-platform solution, but does the job wrapping the Win32 stuff nicely:
https://github.com/jameswynn/simplefilewatcher
There is some public-domain code here. My current project uses this (inherited from previous developers). It works pretty well but we do miss notifications for reasons that are unclear (and possibly not caused by this code).
Note that the Win32 API here has some limitations which make it difficult/impossible to avoid missing notifications. Background and alleged work-round for the API are here
http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.created%28v=vs.71%29.aspx the above does throgh C#, we can always write a COM Wrapper
This is an example of ReadDirectoryChangesW, written in go
kernel32dll := w32.NewKernel32DLL()
dirPath := "C://test_dir"
// Get the HANDLE of the target directory
hDir, _ := kernel32dll.CreateFile(dirPath,
w32.FILE_LIST_DIRECTORY,
w32.FILE_SHARE_READ|w32.FILE_SHARE_WRITE|w32.FILE_SHARE_DELETE,
0,
w32.OPEN_EXISTING,
w32.FILE_FLAG_BACKUP_SEMANTICS|w32.FILE_FLAG_OVERLAPPED,
0,
)
defer kernel32dll.CloseHandle(hDir) // close the handle when the program exit
var maxBufferSize uint32 = 96 // depend on you.
buffer := make([]uint8, maxBufferSize)
// a function for reset the data.
memset := func(a []uint8, v uint8) {
for i := range a {
a[i] = v
}
}
// a function for get the filename
getName := func(offset, fileNameLength uint32) string {
size := fileNameLength / 2
filename := make([]uint16, size)
var i uint32 = 0
for i = 0; i < size; i++ {
filename[i] = binary.LittleEndian.Uint16([]byte{buffer[offset+2*i], buffer[offset+2*i+1]})
}
return syscall.UTF16ToString(filename)
}
var record w32.FILE_NOTIFY_INFORMATION
for {
var dwBytes uint32 = 0
memset(buffer, 0) // clear the buffer for use again.
kernel32dll.ReadDirectoryChanges(hDir,
uintptr(unsafe.Pointer(&buffer[0])),
maxBufferSize,
true, // bWatchSubtree
w32.FILE_NOTIFY_CHANGE_LAST_WRITE|w32.FILE_NOTIFY_CHANGE_CREATION|w32.FILE_NOTIFY_CHANGE_FILE_NAME,
&dwBytes,
nil,
0,
)
if dwBytes == 0 { // if successful dwBytes is the number bytes used, or zero for Failed.
fmt.Printf("Buffer overflow! max-size:%d\n", maxBufferSize)
return
}
record = *(*w32.FILE_NOTIFY_INFORMATION)(unsafe.Pointer(&buffer[0]))
// There may be many FILE_NOTIFY_INFORMATION. For example, if you rename the file, it will trigger the FILE_ACTION_RENAMED_OLD_NAME and FILE_ACTION_RENAMED_NEW_NAM
var offsetFilename uint32 = 12 // The 12 is calculated from FILE_NOTIFY_INFORMATION.{NextEntryOffset, Action, FileName Length} => they are uint32 => 4*3=12
for {
switch record.Action {
case w32.FILE_ACTION_ADDED:
fmt.Println("FILE_ACTION_ADDED")
case w32.FILE_ACTION_REMOVED:
fmt.Println("FILE_ACTION_REMOVED")
return
case w32.FILE_ACTION_MODIFIED:
fmt.Println("FILE_ACTION_MODIFIED")
case w32.FILE_ACTION_RENAMED_OLD_NAME:
fmt.Println("FILE_ACTION_RENAMED_OLD_NAME")
case w32.FILE_ACTION_RENAMED_NEW_NAME:
fmt.Println("FILE_ACTION_RENAMED_NEW_NAME")
default:
break
}
fmt.Println(getName(offsetFilename, record.FileNameLength))
if record.NextEntryOffset == 0 {
break
}
offsetFilename = record.NextEntryOffset + 12
record = *(*w32.FILE_NOTIFY_INFORMATION)(unsafe.Pointer(uintptr(unsafe.Pointer(&buffer[0])) + uintptr(record.NextEntryOffset)))
}
}
You can go here to get the complete code.
https://github.com/CarsonSlovoka/go-pkg/blob/cf4a28372b05458d715ab118d4ce888b2727ac4d/v2/w32/kernel32_func_test.go#L465-L597