I have to check a value of TEdit->Text when a user leaves it and return him to the TEdit, if the value is wrong. The code below works nice in a VCL but it doesn't work in a FMX. So it beeps but doesn't return.
void __fastcall TForm1::Edit1Exit(TObject *Sender)
{
if (Edit1->Text != "123")
{
Beep();
Edit1->SetFocus();
}
}
It is in a simple form with 2 TEdits only. What I do wrong and how to do it right?
I'll provide a solution in Delphi Firemonkey. Hopefully, the same principles apply in C++ Firemonkey. The following code replaces the invalid text in Edit1 with the word 'Invalid' and returns the focus to Edit1, with 'Invalid' selected ready for overtyping.
procedure TForm1.Edit1Validate(Sender: TObject; var Text: string);
begin
if Text <> '123' then
begin
Text := 'Invalid';
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize(nil,
procedure
begin
Edit1.SetFocus;
end);
end).Start;
end;
end;
I wrote a simple program:
void __fastcall TForm1::Edit1Exit(TObject *Sender)
{
TThread::CreateAnonymousThread(
[=]()
{
TThread::Synchronize (NULL, [=](){
Memo1->Lines->Add("Edit1->SetFocus()");
});
})->Start();
Memo1->Lines->Add("Edit1 exit");
}
//--------------------------------------------------------------------------
void __fastcall TForm1::Edit2Enter(TObject *Sender)
{
Memo1->Lines->Add("Edit2 enter");
}
There are three controls on a form: 2 TEdits and a TMemo1. When I run it and jump from the TEdit1 to the TEdit2, I have on the memo:
Edit1 exit
Edit2 enter
Edit1->SetFocus()
Without threads, I would have
Edit1->SetFocus()
Edit1 exit
Edit2 enter
With threads, but without Synchronize I have
Edit1 exit
Edit1->SetFocus()
Edit2 enter
So, what Synchronize() synchronizes with what stays unclear for me.
Related
Enums are/will be the Option replacements for Business Central 365. Recently I had an occasion to use a few and get my feet wet, so to speak. As seems to be the case far too often, about 80% of the functionality you need is readily available, but the remaining 20% takes more work than it should.
In the case of Enums, you get a Text list of Names and an Integer list of associated Ordinal values, but you do NOT get a list of Captions. In the partial example Enum FourStates below, Default, OH and TX are the Names, 0, 1 and 2 are the Ordinals and space, Ohio and Texas are the captions. Note that Ordinals are the defined numeric values, NOT the indexes. Perfectly valid Ordinals in the example below could be 1, 5 and 7.
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
If you define a Table or Page field as an Enum, then the captions are displayed in the dropdowns. To get the Caption you can use Format(EnumType::Name) but we needed to iterate all of the Captions for a given Enum.
After digging around in some blogs and the documentation, here is a summary of what I found.
First, there is a major limitation because Captions can ONLY be processed within the context of an Enum type and, at least as of now, the Business Central 365 SaaS solution has no support for generic Enums (e.g. EnumRef like RecordRef and FieldRef). This means a Text list of Captions must be created on an Enum by Enum basis.
However, once the Captions list has been created, then using the combination of Name, Ordinal and Caption Lists of [Text], you can then write general purpose code that will work for any Enum.
A Gotcha, when you use the TEnum code snippet in VS Code, it defaults the first element to 0. We have learned to either make that the Default as the partial code above does, or change it to 1. The reason for this is because if there is any chance that you will ever use an Enum with the StrMenu command, StrMenu defaults 0 as the Cancel return value. In that case, you could never select the 0 Ordinal Enum because ti would be treated as Canceled.
To create an instance of an Enum to use within your code, create a Var variable similar to MyChoices: Enum "Defined Choices";. If you also define a MyChoice: Enum: "Defined Choices", you can compare them with something like if MyChoice = MyChoices::FirstChoice then, etc.
Below is some sample code with a few different Enum styles along with a method that allows you to create a List of [Text] for the Captions. Again, that has to be coded for each specific Enum.
a. Within VS Code, use AL Go to create a new HelloWorld app
b. Change the app.json file Name and Publisher, I named mine EnumHelper
NOTE: We always define a Publisher because you cannot filter for Default Publisher within SaaS
c. Replace all of the code inside the HelloWorld.al file with the code below
NOTE: To simplify things, everything below is all in the same file
d. The code is a PageExtension for the Chart of Accounts Page and runs off the OnOpenPage trigger
This allows the code to be easily called without requiring a bunch of setup code.
Here is the code that allows you to create a Captions list. The variable myOrdinal is an Integer and Flagged is a defined Enum. Note the Enum::EnumName, similar to Page::PageName or Database::TableName.
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
All of the code (sorry, it didn't format exactly correctly)
enum 50200 FourStates
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
value(3; NC) { Caption = 'North Carolina'; }
value(4; IA) { Caption = 'Iowa'; }
value(5; MO) { Caption = 'Missouri'; }
}
enum 50201 Flagged
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Bold) { Caption = 'Bold'; }
value(2; ITalic) { Caption = 'Italid '; }
value(4; Underline) { Caption = 'Underline'; }
value(8; BoldItalic) { Caption = 'Bold & Italic'; }
value(16; BoldUnderline) { Caption = 'Bold & Underline '; }
value(32; ItalicUnderline) { Caption = 'Italic & Underline'; }
value(64; All3Options) { Caption = 'All 3 Options'; }
}
enum 50202 Randomized
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(7; Good) { Caption = 'The Good'; }
value(5; Bad) { Caption = 'The Bad'; }
value(11; Ugly) { Caption = 'The Ugly'; }
}
enum 50203 ProcessFlowOptions
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Flagged) { Caption = 'Flagged'; }
value(2; Randomized) { Caption = 'Randomized'; }
value(4; FourStates) { Caption = 'FourStates'; }
}
pageextension 50200 "Chart of Accounts EH" extends "Chart of Accounts"
{
var
// Enum instance variables.
myFlagged: Enum Flagged;
myFourStates: Enum FourStates;
myRandomized: Enum Randomized;
trigger OnOpenPage();
begin
case UserID.ToLower() of
'larry':
Message('Hello Larry, this is an extension for October testing.');
'vicki':
Message('Good morning Vicki, this is an extension for October testing.');
else
if Confirm('Hello %1 from EnumHelper.\\Click Yes to process or no to cancel.', true, UserID) then begin
ProcessEnumerations();
end;
end;
end;
local procedure ProcessEnumerations()
var
allLines: TextBuilder;
randomCaptions: List of [Text];
flaggedCaptions: List of [Text];
fourStatesCaptions: List of [Text];
begin
GetEnumCaptions(randomCaptions, flaggedCaptions, fourStatesCaptions);
IterateEnumNamesOrdinalsAndCaptions(allLines, randomCaptions, flaggedCaptions, fourStatesCaptions);
Message(allLines.ToText());
end;
local procedure GetEnumCaptions(randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
GetCaptions(randomCaptions, ProcessFlowOptions::Randomized);
GetCaptions(flaggedCaptions, ProcessFlowOptions::Flagged);
GetCaptions(fourStatesCaptions, ProcessFlowOptions::FourStates);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(allLines: TextBuilder; randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
IterateEnumNamesOrdinalsAndCaptions('Flagged Enum', allLines, myFlagged.Names, myFlagged.Ordinals, flaggedCaptions);
IterateEnumNamesOrdinalsAndCaptions('Randomized Enum', allLines, myRandomized.Names, myRandomized.Ordinals, randomCaptions);
IterateEnumNamesOrdinalsAndCaptions('FourStates Enum', allLines, myFourStates.Names, myFourStates.Ordinals, fourStatesCaptions);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(title: Text; allLines: TextBuilder; enumNames: List of [Text]; enumOrdinals: List of [Integer]; enumCaptions: List of [Text])
var
i: Integer;
enumLine: TextBuilder;
enumLines: TextBuilder;
begin
allLines.AppendLine(title);
allLines.appendLine();
for i := 1 to enumNames.Count do begin
Clear(enumLine);
enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''',');
enumLine.AppendLine('EnumOrdinal: ' + Format(enumOrdinals.Get(i)) + ',');
enumLine.AppendLine('EnumCaption: ''' + enumCaptions.Get(i) + '''.');
//enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''', EnumOrdinal: ' + ordinal + ', EnumCaption: ''' + enumCaptions.Get(i) + '''');
enumLines.AppendLine(enumLine.ToText());
end;
allLines.AppendLine(enumLines.ToText());
end;
local procedure GetCaptions(captions: List of [Text]; processFlowOption: Enum ProcessFlowOptions)
var
myOrdinal: Integer;
myProcessFlowOptions: Enum ProcessFlowOptions;
begin
// Load captions by iterating specific Enums.
case processFlowOption of
myProcessFlowOptions::Flagged:
begin
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::Randomized:
begin
foreach myOrdinal in Randomized.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Randomized.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::FourStates:
begin
foreach myOrdinal in FourStates.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::FourStates.FromInteger(myOrdinal)));
end;
end;
end;
end;
}
Enjoy
I have implemented a better workaround for BC14. That should work on newer versions also, but I have tested on BC14 only.
var
RecRef: RecordRef;
FRef: FieldRef;
OptionNames: List of [Text];
OptionCaptions: List of [Text];
i: Integer;
RecRef.GetTable(Rec); // Some record
FRef := RecRef.Field(20); // Some field of type Enum
OptionNames := FRef.OptionMembers().Split(',');
OptionCaptions := FRef.OptionCaption().Split(',');
for i := 1 to OptionNames.Count() do begin
Evaluate(FRef, OptionNames.Get(i));
Message('Ordinal = %1\Name = %2\Caption = %3',
format(FRef, 0, 9),
OptionNames.Get(i),
format(FRef, 0, 1)); // or OptionCaptions.Get(i)
end;
Our InnoSetup uses an external dll to grab and check an xml file for the paths to data files.
This works fine in most cases, Windows XP, 7, 10 (32/64bit). But for some few users this fails and for me it fails in Crossover 19 for macOS 10.15.
So first I added a delayload to the inno script to get past the "cannot import dll" runtime error.
But then I get "could not call proc" runtime error.
The InnoSetup debugger pointed me at procedure GetExultGamePaths of our script.
Code in our dll:
extern "C" {
__declspec(dllexport) void __stdcall GetExultGamePaths(char *ExultDir, char *BGPath, char *SIPath, int MaxPath) {
MessageBoxDebug(nullptr, ExultDir, "ExultDir", MB_OK);
MessageBoxDebug(nullptr, BGPath, "BGPath", MB_OK);
MessageBoxDebug(nullptr, SIPath, "SIPath", MB_OK);
int p_size = strlen(ExultDir) + strlen("/exult.cfg") + MAX_STRLEN;
char *p = new char[p_size];
// Get the complete path for the config file
Path config_path(ExultDir);
config_path.AddString("exult.cfg");
config_path.GetString(p, p_size);
setup_program_paths();
const static char *si_pathdef = CFG_SI_NAME;
const static char *bg_pathdef = CFG_BG_NAME;
MessageBoxDebug(nullptr, ExultDir, p, MB_OK);
try {
// Open config file
Configuration config;
if (get_system_path("<CONFIG>") == ".")
config.read_config_file(p);
else
config.read_config_file("exult.cfg");
std::string dir;
// SI Path
config.value("config/disk/game/serpentisle/path", dir, si_pathdef);
if (dir != si_pathdef) {
Path si(ExultDir);
si.AddString(dir.c_str());
si.GetString(SIPath, MaxPath);
} else {
std::strncpy(SIPath, si_pathdef, MaxPath);
}
// BG Path
config.value("config/disk/game/blackgate/path", dir, bg_pathdef);
if (dir != bg_pathdef) {
Path bg(ExultDir);
bg.AddString(dir.c_str());
bg.GetString(BGPath, MaxPath);
} else {
std::strncpy(BGPath, bg_pathdef, MaxPath);
}
} catch (...) {
std::strncpy(BGPath, bg_pathdef, MaxPath);
std::strncpy(SIPath, si_pathdef, MaxPath);
}
delete [] p;
}
The part of the InnoSetup Script
procedure GetExultGamePaths(sExultDir, sBGPath, sSIPath: String; iMaxPath: Integer);
external 'GetExultGamePaths#files:exconfig.dll stdcall delayload';
procedure CurPageChanged(CurPageID: Integer);
var
sBGPath: String;
sSIPath: String;
begin
if CurPageID = DataDirPage.ID then begin
if bSetPaths = False then begin
setlength(sBGPath, 1024);
setlength(sSIPath, 1024);
GetExultGamePaths(ExpandConstant('{app}'), sBGPath, sSIPath, 1023 );
BGEdit.Text := sBGPath;
SIEdit.Text := sSIPath;
end;
end;
end;
The GetExultGamePaths(ExpandConstant('{app}'), sBGPath, sSIPath, 1023 ); is what is producing the "could not call proc" runtime error.
I have no idea why that fails on only a few systems.
The full code for our dll and the script is at https://github.com/exult/exult/blob/master/win32/
The dll's code is in exconfig.* and the InnoSetup script is in exult_installer.iss
Through the help of Piotr in our Wine bug report https://bugs.winehq.org/show_bug.cgi?id=49033 it was discovered that using the mingw tool "dllwrap" is broken and added invalid relocation data.
NOT using dllwrap was the solution and fixed it for my Wine/Crossover install. Still waiting for the last user to report back who had this problem on a real Windows 10 installation.
Thanks for your help
In my shell extension, I want to mimic explorer's behavior and show 'This folder is empty' message when in fact my folder is empty:
However, I can't accomplish it.
Using API Monitor, I see that when explorer refreshes an empty folder, IEnumIDList::Next() is returning the following:
Meaning, that the 'next' item returned is NULL, the number of items is 0 and the result is S_FALSE.
As mentioned, I tried to mimic the return values, and indeed no items are loaded for the folder, but no message appear either.
So what API would trigger this message?
Your IEnumIDList implementation must implement IObjectWithSite. Sample of implementation:
var
ServiceProvider: IServiceProvider;
ShellBrowser: IShellBrowser;
ShellView: IShellView;
FolderView2: IFolderView2;
begin
if not Assigned(ASite) then Exit;
OleCheck(ASite.QueryInterface(IServiceProvider, ServiceProvider));
try
OleCheck(ServiceProvider.QueryService(SID_STopLevelBrowser, IShellBrowser, ShellBrowser));
try
OleCheck(ShellBrowser.QueryActiveShellView(ShellView));
try
OleCheck(ShellView.QueryInterface(IFolderView2, FolderView2));
try
FolderView2.SetText(FVST_EMPTYTEXT, 'The message you want to see');
finally
FolderView2 := nil;
end;
finally
ShellView := nil;
end;
finally
ShellBrowser := nil;
end;
finally
ServiceProvider := nil;
end;
end;
Result:
Also you can use the same code in your IShellFolder implementation.
This is an old question but I write my answer. It might be useful to someone
You can change the empty folder view text in IShellFolder::CreateViewObject method, like below:
if (riid == IID_IShellView)
{
m_hwndOwner = hwndOwner;
SFV_CREATE sfvData = { sizeof sfvData };
const CComQIPtr<IShellFolder> spFolder = GetUnknown();
sfvData.psfvcb = this;
sfvData.pshf = spFolder;
auto hr = SHCreateShellFolderView(&sfvData, reinterpret_cast<IShellView**>(ppv));
// Here we change the text
auto pSV = *reinterpret_cast<IShellView**>(ppv);
CComPtr<IFolderView2> pFV;
pSV->QueryInterface(IID_PPV_ARGS(&pFV));
pFV->SetText(FVST_EMPTYTEXT, L"Put your text here!");
//-
return hr;
}
In a gui I want to modify the text a user inserts in a GtkEntry. eg if the user enters 'joHn doe', my gui should see this is not a nicely formatted name and changes this into 'John Doe'.
I connect the a handler to "changed" signal as described in eg GtkEntry text change signal. The problem that occurs is if I change the entry in my signal handler, the "changed" signal is emitted again and again until kingdom comes.
I currently prevent this by doing a string comparison, and I only change the text in the GtkEntryBuffer if the text "namified" version is unequal to the text inside the entry. However I feel like as programmer I should be able to change the the text inside the entry without that the changed handler is called over and over again.
The changed signal handler is:
void nameify_entry ( GtkEditable* editable, gpointer data )
{
gchar* nameified;
const gchar *entry_text;
entry_text = gtk_entry_get_text( GTK_ENTRY(editable) );
nameified = nameify(entry_text);
/*is it possible to change the buffer without this using this string
comparison, without the "change" signal being emitted over and over again?*/
if ( g_strcmp0(entry_text, nameified) != 0 ){
GtkEntryBuffer* buf = gtk_entry_get_buffer(GTK_ENTRY(editable) );
gtk_entry_buffer_set_text( buf, nameified, -1 );
}
g_free(nameified);
}
and my nameify function is:
/*removes characters that should not belong to a name*/
gchar*
nameify ( const char* cstr )
{
const char* c;
gchar* ret_val;
GString* s = g_string_new("");
gboolean uppercase_next = TRUE;
g_debug( "string = %s", cstr);
for ( c = cstr; *c != '0'; c = g_utf8_next_char(c) ) {
gunichar cp = g_utf8_get_char(c);
if ( cp == 0 ) break;
if ( g_unichar_isalpha( cp ) ){
if ( uppercase_next ){
g_string_append_unichar( s, g_unichar_toupper(cp) );
uppercase_next = FALSE;
}
else{
g_string_append_unichar(s,g_unichar_tolower(cp));
}
}
if ( cp == '-' ){
g_string_append_unichar( s, cp);
uppercase_next = TRUE;
}
if ( cp == ' '){
g_string_append_unichar( s, cp);
uppercase_next = TRUE;
}
}
ret_val = s->str;
g_string_free(s, FALSE);
return ret_val;
}
any help is most welcome.
It's not really handy to connect to the 'changed' signal, but more appropriate to connect to the 'insert-text' signal. Even better to have the default 'insert-text' handler update the entry. Than use g_signal_connect_after on the 'insert-text' signal to update the text in the entry this prevents the changed signal to run infinitely. This should also be done to the 'delete-text' signal, because if a user deletes a capital letter, the capital should be removed and the second should be capitalized.
so on creation run:
g_signal_connect_after( entry, "insert-text", G_CALLBACK(name_insert_after), NULL );
g_signal_connect_after( entry, "delete-text", G_CALLBACK(name_delete_after), NULL );
Then you can have these signal handlers:
void
name_insert_after (GtkEditable* edit,
gchar* new_text,
gint new_length,
gpointer position,
gpointer data)
{
/*prevent compiler warnings about unused variables*/
(void) new_text; (void) new_length; (void) position; (void) data;
const gchar* content = gtk_entry_get_text( GTK_ENTRY(edit) );
gchar* modified = nameify( content);
gtk_entry_set_text(GTK_ENTRY(edit),modified);
g_free(modified);
}
void
name_delete_after (GtkEditable* edit,
gint start_pos,
gint end_pos,
gpointer data)
{
/*no op cast to prevent compiler warnings*/
(void) start_pos; (void) end_pos; (void) data;
/*get text and modify the entry*/
int cursor_pos = gtk_editable_get_position(edit);
const gchar* content = gtk_entry_get_text( GTK_ENTRY(edit) );
gchar* modified = nameify( content);
gtk_entry_set_text(GTK_ENTRY(edit),modified);
gtk_editable_set_position(edit, cursor_pos);
g_free(modified);
}
and these can than be used with the nameify function in the original post.
you might even provide a function pointer at the data instead of 'NULL' to use this one handler with different functions that are able to modify the string in the entry.
For your requirement insert-text signal seems more appropriate. insert-text is made available to make possible changes to the text before being entered. You can make use of the callback function template insert_text_handler part of the description of GtkEditable. You can make use of nameify with changes to the function (as you will not get the whole text but parts of text or chars; simplest modification could be to declare uppercase_next static) for making modification to the text.
Hope this helps!
The quickest solution in my opinion would be to temporarily block your callback from being called.
The g_signal_connect group of functions each return a "handler_id" of type gulong. You will have to store this id, pass it to your callback using the "userdata" argument (or just use a global static variable instead), then put your text manipulation code in between a g_signal_handler_block/g_signal_handler_unblock pair.
Connecting to insert-text and delete-text is the right idea but you want to connect using g_signal_connect. If you use g_signal_connect_after then the incorrect text has already been displayed before you correct it, which might cause the display to flicker. Also you need to block your signal handlers when calling gtk_entry_set_text as this emits delete-text followed by insert-text. If you don't block the signals you will recursively call your signal handlers. Remember GObject signals are just function calls. Emitting a signal is the same as calling the handlers directly from your code.
I would suggest having a handler for insert-text that looks to see if it needs to change the new input. If it does then create a new string and do this as per the GtkEditable documentation
g_signal_handlers_block_by_func (editable, insert_text_handler, data);
gtk_editable_insert_text (editable, new-text, g_strlen(new_text) , position);
g_signal_handlers_unblock_by_func (editable, insert_text_handler, data);
g_signal_stop_emission_by_name (editable, "insert_text");
If you don't need to change the input just return.
For the delete-text handler I'd look to see if you need to change the text (remembering that nothing will have been deleted yet) and if so update the whole string with
g_signal_handlers_block_by_func (editable, insert_text_handler, data);
g_signal_handlers_block_by_func (editable, delete_text_handler, data);
gtk_entry_set_text (GKT_ENTRY (editable), new-text);
g_signal_handlers_unblock_by_func (editable, delete_text_handler, data);
g_signal_handlers_unblock_by_func (editable, insert_text_handler, data);
g_signal_stop_emission_by_name (editable, "delete_text");
again just return if you don't need to change the text.
simpler than blocking and unblocking your signal just have a boolean:
myHandler(...){
static int recursing=0;
if(recursing){
recursing=0;
return;
}
... logic to decide if a change is needed
recursing=1;
gtk_entru_set_text(...);
... will recurse to your hander, which will clear the recursing variable and resume here
}
As part of my Visual Studio utilities add-in SamTools, I have a mouse input routine that catches Ctrl+MouseWheel and sends a pageup/pagedown command to the active text window. Visual Studio 2010 added a new "feature" that uses that gesture for zoom in/out (barf). Currently, my add-in does send the scrolling command, but Visual Studio still changes the font size because I'm not eating the input.
I set my hook with a call to SetWindowsHookEx. Here's the callback code. My question is: is the best way to prevent Visual Studio from handling the Ctrl+MouseWheel input as a zoom command to simply not call CallNextHookEx when I get a mouse wheel event with the Ctrl key down?
(Please bear in mind this is some old code of mine.) :)
private IntPtr MouseCallback(int code, UIntPtr wParam, ref MOUSEHOOKSTRUCTEX lParam)
{
try
{
// the callback runs twice for each action - this is the latch
if (enterHook)
{
enterHook = false;
if (code >= 0)
{
int x = lParam.mstruct.pt.X;
int y = lParam.mstruct.pt.Y;
uint action = wParam.ToUInt32();
switch (action)
{
case WM_MOUSEWHEEL:
OnMouseWheel(new MouseEventArgs(MouseButtons.None, 0, x, y, ((short)HIWORD(lParam.mouseData)) / (int)WHEEL_DELTA));
break;
default:
// don't do anything special
break;
}
}
}
else
{
enterHook = true;
}
}
catch
{
// can't let an exception get through or VS will crash
}
return CallNextHookEx(mouseHandle, code, wParam, ref lParam);
}
And here's the code that executes in response to the MouseWheel event:
void mouse_enhancer_MouseWheel( object sender, System.Windows.Forms.MouseEventArgs e )
{
try
{
if ( Keyboard.GetKeyState( System.Windows.Forms.Keys.ControlKey ).IsDown && Connect.ApplicationObject.ActiveWindow.Type == vsWindowType.vsWindowTypeDocument )
{
int clicks = e.Delta;
if (e.Delta < 0)
{
Connect.ApplicationObject.ExecuteCommand( "Edit.ScrollPageDown", "" );
}
else
{
Connect.ApplicationObject.ExecuteCommand( "Edit.ScrollPageUp", "" );
}
}
}
catch ( System.Runtime.InteropServices.COMException )
{
// this occurs if ctrl+wheel is activated on a drop-down list. just ignore it.
}
}
PS: SamTools is open source (GPL) - you can download it from the link and the source is in the installer.
PSS: Ctrl+[+] and Ctrl+[-] are better for zooming. Let Ctrl+MouseWheel scroll (the vastly more commonly used command).
According to MSDN, it's possible to toss mouse messages that you process. Here's the recommendation:
If nCode is less than zero, the hook
procedure must return the value
returned by CallNextHookEx.
If nCode is greater than or equal to
zero, and the hook procedure did not
process the message, it is highly
recommended that you call
CallNextHookEx and return the value it
returns; otherwise, other applications
that have installed WH_MOUSE hooks
will not receive hook notifications
and may behave incorrectly as a
result. If the hook procedure
processed the message, it may return a
nonzero value to prevent the system
from passing the message to the target
window procedure.
In other words, if your mouse callback ends up using the mouse message, you don't have to call the next CallNextHookEx -- just return a nonzero value and (in theory, at least) the mouse movement should get swallowed. If that doesn't work the way you want, comment and we can iterate.
BTW, another possible alternative: it's possible that VS's mapping to the mouse wheel shows up in the Tools...Customize... UI, just like key mappings do. In that case, you could simply remap your add-in's commands instead of working at the hook level. But it's also posible (likely?) that this gesture is hard-coded.