Title: List network providers, domains, servers, drives, and printers.
Question: How can I mimic the Network Neighborhood?
How can I search for a server on the network?
How can I find all the computers on a network?
How can I find all the shared resources on a server?
How can I search the network for printers?
Answer:
Tutorial:
List network providers, domains, servers, drives, and printers in Delphi.
----------------------------------------------------------
Author : Phil Gilmore
Revision : 0
Published : 11/12/2003
Updated : 12/10/2003
------------------
Abstract
------------------
Thank you for reading this tutorial. Please take a moment to rate the tutorial so that I know how helpful it was. If it lacks something you're looking for, please leave a comment.
I've taken all the pertinent concepts discussed in this document, wrapped them in a class, and put together a working demo for you to play with. I have sent the zip to the moderators three times. Since it's still not attached and I have no reply (it's been a month now...), I've put the zip file on the web and linked this article the its URL. This is borrowed web space. A big thanks to my anomymous donor for providing this space!
This tutorial will describe how to mimic the network neighborhood (of late, called "My network places") using Delphi. You can do more than list all resources, though. You can query just servers, just disks or just printers, or list them all and figure out which is a server, disk, or printer. You can browse the entire network from top to bottom.
------------------
Platform differences
------------------
This tutorial and demo application were developed under Windows 2003. While I suspect that the behavior will be uniform across Windows 2000 and XP, I might expect slightly different behavior under Win9x, ME or even NT4. If you see something different, let me know all the details and I will update the tutorial. In particular, I don't know how other providers will be presented. I can only test resources under the Windows Networking Services provider, so I don't know how Netware or other providers might behave. Furthermore, the large network I've tested this on is at work, and we use ActiveDirectory, which may change the behavior as well.
------------------
Network hierarchy and resource types
------------------
In the network, there is a hierarchy of resources. These resources include network providers, domains / workgroups, servers, disks / printers. Hierarchically, they should be found in that order. All of the above are considered to be CONTAINERS except disks and printers. Disks and printers are considered to be CONNECTABLES.
All containers can be queried for subresources. Each time you query a container, you are "drilling down" into the hierarchy. Disks and printers cannot be queried for subresources, so they are always at the bottom of the hierarchy. The network providers are at the top of the hierarchy.
We can query any group of network resources by specifying the parent in the hierarchy. In the case of the top of the hierarchy, we can specify a NIL for the parent, which gives us a starting point for drilling down.
------------------
Querying the top-level of the hierarchy.
------------------
We can query any group of network resources using three API calls. The first is WNETOPENENUN, which gives us a handle to an enumeration if we pass valid parameters. The second (WNETEnumResource) uses that handle to actually search the network and give us some names and data. The third is WNETCLOSENUM, and is used to free a window handle used by the previous two functions. Here we will build an application that will show how to use these.
Create a new project. To the main form, drop a tlistbox component and a tbutton. Name the button cmdTopLevel. Edit the ONCLICK event for that button and add this line to the event:
DoToplevel;
Now add the following to the private section of the form class:
procedure DoTopLevel;
Class completion now gives us this procedure.
procedure TForm1.DoTopLevel;
begin
end;
The procedure will call these two functions to return a list of network resources at the top level of the network hierarchy. Here is an example of the first function call.
rc := WNETOpenEnum(
RESOURCE_GLOBALNET, //includes all resource objects
RESOURCETYPE_ANY, //include disks and printers
RESOURCEUSAGE_ALL, //include containers and non-containers.
NIL, //parent of the group to query.
henum //handle for this enumeration.
);
The top level of the hierarchy has no parent, so we pass nil as the parent. All other parameters that we've specified indicate, in general, to return everything possible. The whole purpose of this function is to get the HENUM handle.
The purpose of the second function is to take that HENUM handle and get network resources out of it. We can call the second function like this:
rc := WNETEnumResource(
henum, //handle to an enumeration
ccount, //max resources to return
buf, //buffer to hold resource data
bufsize //size of the buffer.
);
Just like the first one, this is a very simple call. There are only a few rules to making it work. First, your ccount must specify a max number. $FFFFFFFF will return all possible values. This is a VAR parameter, so its value after the call will be the number of resources that were actually found.
Bufsize must be the size of the buffer specified, and the buffer should already be allocated. The buffer will be filled with back-to-back NETRESOURCEA structures. They can easily be plucked out by typecasting the buffer to an array. We'll do that with this array type:
Type
TNetResArray = Array [0..32767] Of NETRESOURCEA;
PNetResArray = ^TNetResArray;
Take a moment now to look at the NETRESOURCE record structure. The REMOTENAME member is of primary interest to us right now. Most of the rest of it is useful for determining whether a resource is a server, drive, printer, or otherwise. Here is the code in its entirety for you to put into the DoTopLevel procedure:
procedure TForm1.DoTopLevel;
Type
TNetResArray = Array [0..32767] Of NETRESOURCEA;
PNetResArray = ^TNetResArray;
var
//required for both functions:
rc:Cardinal;
henum:THandle;
//required for second function:
j:Integer;
ccount:Cardinal;
buf:Pointer;
bufsize:Cardinal;
nr:NETRESOURCEA;
begin
//----------First function:---------------
//This will give us a handle to a network resource enumeration.
rc := WNETOpenEnum(
RESOURCE_GLOBALNET, //includes all resource objects
RESOURCETYPE_ANY, //include disks and printers
RESOURCEUSAGE_ALL, //include containers and non-containers.
NIL, //parent of the group to query.
henum //handle for this enumeration.
);
//check the result
If (rc NO_ERROR) Then Begin
ShowMessage('Cannot get a handle to an enumeration.');
Exit;
End;
//----------Second function:---------------
//This will go through each item in the enumeration and display it.
bufsize := 100000; //100kb buffer to store results.
getmem(buf, bufsize); //allocate the buffer
ccount := $FFFFFFFF; //this is the number of resources to return. This will return all possible resources.
rc := WNETEnumResource(
henum, //handle to an enumeration
ccount, //max resources to return
buf, //buffer to hold resource data
bufsize //size of the buffer.
);
WNetcloseEnum(henum);
//check result
If (rc NO_ERROR) Then Begin
ShowMessage('Error reading enumeration.');
Exit;
End;
If (ccount=0) Then Begin
ShowMessage('No resources found.');
Exit;
End;
//show results
listbox1.Clear;
For j:= 0 to ccount-1 Do Begin
//get a NETRESOURCEA structure out of the buffer
nr := pnetresarray(buf)[j];
//add the name of this resource to the list.
listbox1.Items.Add(nr.lpRemoteName);
End;
//free buffer
freemem(buf, bufsize);
end;
Go ahead and give the program a try. You should see the top-level of your network hierarchy appear in the listbox. Wasn't that easy?
------------------
Drilling Down
------------------
Now that we've built a fine application for viewing network resources, scrap it. The old one was simple enough that it's easier to start over than to change it. Create a new application and add a TListView component and a TButton component to it. Name the button cmdTopLevel. Add another TButton and name it cmdDrillDown. Change the Viewstyle for the listview to VSREPORT. Double-click on it and add 6 columns. Size them as you like. Set their captions to: RemoteName, Localname, Provider, Comment, Scope, Type.
You may have noticed before that the second function is always the same and is well-suited for its own function. We'll do that first. We'll do it a little differently, though. We need to preserve the current NETRESOURCEA structures for the purpose of drilling down into the hierarchy, so we're going to make the buffer a private member of the form class.
private
ccount:Cardinal;
buf:Pointer;
bufsize:Cardinal;
We'll initialize it in the ONCREATE event.
procedure TForm1.FormCreate(Sender: TObject);
begin
bufsize := 100000;
getmem(buf, bufsize);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
freemem(buf, bufsize);
buf := NIL;
end;
Next, we code our READNETENUM procedure. Add the prototype to the private section of the form class.
private
Procedure ReadNetEnum( henum:thandle );
Using code completion, we get our prototype class, and change it as follows:
procedure TForm1.ReadNetEnum(henum:THandle);
var
RC:Cardinal;
j:Integer;
li:tlistitem;
nr:NETRESOURCEA;
begin
//get the resource buffer
ccount := $FFFFFFFF;
rc := WNETEnumResource(
henum,
ccount,
buf,
bufsize
);
//check for errors
If (rc NO_ERROR) Then Begin
ShowMessage('Error reading net enumeration.');
Exit;
End;
If (ccount = 0) Then Begin
ShowMessage('No net resources found.');
Exit;
End;
//populate details for this resource into the list.
listview1.Items.Clear;
For j:= 0 to ccount-1 Do Begin
nr := pnetresarray(buf)[j];
li := listview1.Items.Add;
li.Caption := nr.lpRemoteName;
li.SubItems.Add(nr.lpLocalName);
li.SubItems.Add(nr.lpProvider);
li.SubItems.Add(nr.lpComment);
//scope
Case (nethood.resources[j].dwDisplayType) Of
resourcedisplaytype_generic : li.subitems.Add('');
resourcedisplaytype_server : li.subitems.Add('Server');
resourcedisplaytype_share : li.subitems.Add('Share');
resourcedisplaytype_domain : li.SubItems.Add('Domain');
End;
//type
If nr.dwType = resourcetype_disk
Then li.SubItems.Add('Disk')
Else
If nr.dwType = resourcetype_print
Then li.SubItems.Add('Print');
End;
end;
It may seem like a lot of code in there, but there are really just 3 easy steps. 1) Get the buffer, 2) check for errors, and 3) populate the list. Most of code is in the third part, populating the list, which is very simple. Notice that we didn't define our tnetresarray type in the local procedure this time because we'll use it elsewhere in this project, too. Hence, we need to add it to our interface section in the global scope:
Type
TNetResArray = Array [0..32767] Of NETRESOURCEA;
PNetResArray = ^TNetResArray;
Great. Now we add our simplified DOTOPLEVEL procedure to the form class and implement it as follows:
procedure TForm1.dotoplevel;
var
rc:Cardinal;
henum:THandle;
begin
rc := WNETOpenEnum(
RESOURCE_GLOBALNET,
RESOURCETYPE_ANY,
RESOURCEUSAGE_ALL,
NIL,
henum
);
If (rc NO_ERROR) Then Begin
ShowMessage('Cannot get a handle to an enumeration.');
Exit;
End;
readnetenum(henum);
WNetcloseEnum(henum);
end;
Don't forget to call it from your cmdToplevel button's ONCLICK event.
procedure TForm1.cmdTopLevelClick(Sender: TObject);
begin
dotoplevel;
end;
------------------
The DoDrillDown Procedure
------------------
In the cmdDrillDown button, add a call to the DoDrillDown procedure. The only difference between this procedure and the DoTopLevel procedure is the parent that we specify when we open a new enumeration. For the top level of the hierarchy, we specify NIL as the parent. To drill down, we specify one of the NETRESOURCEA structures that we got from a previous call. We can simply duplicate the DOTOPLEVEL procedure as a starting point. Add the declaration to the form class.
private
Procedure DoDrillDown;
Copy the code from dotoplevel into the new dodrilldown procedure.
procedure TForm1.DoDrillDown;
var
rc:Cardinal;
henum:THandle;
begin
rc := WNETOpenEnum(
RESOURCE_GLOBALNET,
RESOURCETYPE_ANY,
RESOURCEUSAGE_ALL,
NIL,
henum
);
If (rc NO_ERROR) Then Begin
ShowMessage('Cannot get a handle to an enumeration.');
Exit;
End;
readnetenum(henum);
WNetcloseEnum(henum);
end;
Now we have one thing left to do. Get a NETRESOURCEA structure and pass it as the parent in the WNETOpenEnum function call. We want to get that from the current array of NETRESOURCEA structures, based on the item that is selected in the listview box. This requires us to know which index in the NETRESOURCEA buffer array to use. We can determine this in two ways. We could rely on the index of the selected item in the listview to be the same index as the corresponding item in the buffer array. This works, but requires that we never change the order of the items in the list and never omit any items. Another way is to loop through the array until we find the same VALUE as that which is selected. We will use this second method because we can then use the flexibility later.
procedure TForm1.DoDrillDown;
var
rc:Cardinal;
henum:THandle;
i, j:Integer;
s1, s2:String;
parentindex:Integer;
parentresource:NETRESOURCEA;
begin
//we require that a selection be made in the list.
i := listview1.ItemIndex;
If (i = (-1)) Then Exit;
//get the index of the parent resource in the buffer array.
parentindex := -1;
s1 := trim(uppercase(listview1.Items[i].Caption));
For j:=0 to ccount-1 Do Begin
s2 := trim(uppercase(pnetresarray(buf)[j].lpRemoteName));
If (s1 = s2) Then Begin
parentindex := j;
break;
End;
End;
If (parentindex = (-1)) Then Begin
ShowMessage('Resource name not found in array.');
exit;
End;
//get the parent resource
parentresource := pnetresarray(buf)[parentindex];
//get subresources
rc := WNETOpenEnum(
RESOURCE_GLOBALNET,
RESOURCETYPE_ANY,
RESOURCEUSAGE_ALL,
@parentresource,
henum
);
If (rc NO_ERROR) Then Begin
ShowMessage('Cannot get a handle to an enumeration.');
Exit;
End;
readnetenum(henum);
WNetcloseEnum(henum);
end;
You may already see that we can further condense the program by taking redundant parts out of dotoplevel and dodrilldown (the part commented as GETSUBRESOURCES) and put it in its own procedure named DoEnumerate and pass it a PARENTRESOURCE:PNETRESOURCEA parameter. Then, dodrilldown would pass it the @PARENTRESOURCE variable address, and dotoplevel would pass NIL. In fact, you could put all of this into a separate unit and make it even more reusable. I'll leave that to your spare time.
After you play with this application long enough, you'll quickly find it annoying to click on an item, then click on a button, item, button, etc. You'll probably want to call the DODRILLDOWN from listview's ONDBLCLICK event.
------------------
What can you do with network resources?
------------------
It may seem obvious how functions could be useful, until you start coding and figure out that you don't have a connection to these network resources, and hence cannot access the drives, printers or other server resources.
Practically, you need these connections in order to do anything with the network resources. This is easy enough to do. The WNETADDCONNECTION function call will create a connection to those resources without mapping a network drives. You would use a UNC (universal naming convention) name in the resource path (i.e. file path) to access it from them on. If you look, you'll find that there are several versions of WNETADDCONNECTION. There are 3 versions, each with both an ANSI and UNICODE version. We could easily use version 2 or 3, since they take a NETRESOURCEA parameter instead of a resource name string, which the first version does.
Using version 2 of the function and using UNC, we have what is called a "deviceless connection". Deviceless means that we did not map a drive. Since we did not map a drive, the connection cannot be remembered (called "Persistent"). If you need to remember these connections or need to map a drive letter to the network resource, use the first version for WNETADDCONNECTION and specify the drive specification as the localname parameter.
The following illustrates the WNETADDCONNECTION using a NETRESOURCEA structure.
procedure TForm1.ConnectToResource(res: PNETRESOURCEA; username, password:String);
begin
WNetAddConnection2(
res^,
PChar(password),
PChar(username),
0
);
end;
Likewise, you can programmatically disconnect from a network resource as well, using WNETCANCELCONNECTION2. Note that even though there is a WNETCANCELCONNECTION function, you would use WNETCANCELCONNECTION2 no matter which version of WNETADDCONNECTION you choose to use. It supercedes WNETCANCELCONNECTION and is the only recommended method.
procedure TForm1.DisconnectFromResource(res: PNETRESOURCEA);
begin
WNetCancelConnection2(
res^.lpRemoteName,
0, //set this to CONNECT_UPDATE_PROFILE to
//remove persistent connections.
true
);
end;
------------------
Constructing your own NETRESOURCE
------------------
There are a few drawbacks to the methods we've used so far. The first is that you can only connect to resources that are exposed. For example, the \C$ share that is present on most machines is not shown in our network neighborhood. You can still connect to it if you can get a NETRESOURCE for it. Likewise, you may want to enumerate subresources for a server without browsing to it (maybe you don't know where it is in the hierarchy).
Both can be solved by creating your own NETRESOURCEA structure. While it is always good to be thorough, sometimes you just don't have all the values to populate the entire structure. There's good news. In most cases, you can enumerate a container or workgroup with as little as a remotename and a provider. Given a remotename (which is almost always going to be your criteria), you can try each of the providers individually until you find the one you're looking for. On my system, there are 3 providers. You could simply call dotoplevel and get the names of the providers, then store them in a tstringlist. Finally, try each one. If your resource doesn't exist under the specified provider, you will get an error on WNETOpenEnum, so you only have to call WNETEnumResourceS once, that is, when you find the correct provider.
------------------
Other things you can do
------------------
Try different values for the dwSCOPE parameter when calling WNETOpenEnum. Each one does something different. To list the drive letters you currently have mapped, specify RESOURCE_REMEMBERED. To list both mapped drive letters as well as connected but unmapped connections, specify RESOURCE_CONNECTED.