Solved

Get List of User Profiles

Posted on 2006-07-14
19
1,044 Views
Last Modified: 2008-09-11
Hello,

I need a way in VB.NET to get a list of users who have profiles on the machine.  I'd rather not do this by just going to the Documents and Settings folder and enumerating folder names, and it needs to be their user names, and not just folder names (since TestUser.046's user name isn't TestUser.046 ;)).  I'd like code tha will work on at least Windows 2000 and Windows XP, and I'd be extra happy if it was NT compatible.  It should also run for Power User.  I have control over the machines, so if this means changing the security on a few keys, that's OK.

Bonus points if I can get the domain they logged in with.

Thanks!
Chris.
0
Comment
Question by:usachrisk1983
  • 7
  • 7
  • 3
  • +1
19 Comments
 
LVL 6

Expert Comment

by:willbdman
ID: 17108202
First you need to add a reference to System.DirectoryServices.dll

Then use this code to add users to a listbox:-

    Private Sub GetUsers()

        'Make DirectoryEntry of Your Computer
        Dim AD As System.DirectoryServices.DirectoryEntry = New System.DirectoryServices.DirectoryEntry("WinNT://" + Environment.MachineName)

        ' For each child in tree
        For Each child As DirectoryServices.DirectoryEntry In AD.Children

            ' If child is user
            If child.SchemaClassName = "User" Then Me.ListBox1.Items.Add(child.Name)

        Next


    End Sub
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17108595
Looks like that is only getting me local accounts on the machine.
0
 
LVL 41

Expert Comment

by:graye
ID: 17110473
There are several steps to the process...

1) Open the registry to get the ProfileList
                key = reg_hklm.OpenSubKey("SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList", False)
2) Loop thru the subkeys
                    For Each keyname In key.GetSubKeyNames
3) Get the SID value under each subkey (or use the keyname as the SID)
                        RawSID = CType(subkey.GetValue("SID", Nothing), Byte())
4) Convert the SID to a Username using the API (Misc.SidToName is just a wrapper class I made around the APIs)
                                If Not IsNothing(RawSID) Then
                                    UserID = Misc.SidToName(RemotePC, RawSID)
                                Else
                                    ' If we don't have an imbedded SID, then we
                                    ' use the key name instead
                                    UserID = Misc.SidToName(RemotePC, keyname)
                                End If

I have a complete example (that does a lot more than just list the names) that works for WinNT thru WinVista.
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17110574
I'd love to see your full example.
0
 
LVL 41

Expert Comment

by:graye
ID: 17110865
Okey dokey... This sample is pulled from a complete application, so it will probably not compile by itself.  It also contains a bunch of stuff that you can safely ignore (like the database stuff and the mounting of registry hives)

    '
    ' Get information about the User Profiles and mount all of the User's
    ' registry hives.
    ' Note: The registry hives mounted here are used in: GetEmail, GetInternet,
    ' GetMapped, GetShares, and GetStartup.  We do not update the "reference
    ' count" for the hive, so it's possible that some other program might
    ' umount the hive before we read from it.
    '
    Public Sub GetProfiles()
        If IsWin32 Then
            Dim DefaultUserProfile, keyname, ImagePath, ProfileName, UserID, LoadedHives() As String
            Dim key, subkey, testkey As RegistryKey
            Dim flags, num_users As Integer
            Dim ld As LongDate
            Dim RawSID() As Byte
            Dim admin, we_mounted_it As Boolean
            Dim dr As SOSOSDataSet.ProfilesRow

            ImagePath = ""
            keyname = ""
            Try
                ' the mounting of registry hives is a tightly held privilege
                ' that is not enabled by default (even for administrators)
                admin = Misc.IsAdmin()

                ' make a list of hives that are currently loaded
                LoadedHives = reg_hku.GetSubKeyNames

                ' get the listing of profiles from the registry
                key = reg_hklm.OpenSubKey("SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList", False)
                If Not IsNothing(key) Then
                    ' loop thru each subkey
                    num_users = 0
                    For Each keyname In key.GetSubKeyNames
                        subkey = key.OpenSubKey(keyname, False)
                        flags = CInt(subkey.GetValue("Flags", -1))
                        RawSID = CType(subkey.GetValue("SID", Nothing), Byte())

                        ' for WinVista, the flags trick doesn't work to weed
                        ' out the non-numan keys
                        If OSVer >= 6.0 And IsNothing(RawSID) Then
                            flags = -1
                        End If

                        ' we only want "human" profiles (WinNT doesn't have
                        ' this flag, but it also doesn't list non-human keys)
                        If flags = 0 Or OSVer = 4.0 Then
                            num_users += 1
                            ImagePath = subkey.GetValue("ProfileImagePath", "").ToString
                            ' We have to substitute the values for our expanded
                            ' enivronmental variables with those of the remote
                            ' PC!
                            If RemotePC <> "" Then
                                ImagePath = ImagePath.Replace(Environment.ExpandEnvironmentVariables("%SystemDrive%"), SystemDrive)
                            End If
                            If Directory.Exists(Misc.ConvUNC(RemotePC, ImagePath)) Then
                                ld.date_hi = CInt(subkey.GetValue("ProfileLoadTimeHigh", 0))
                                ld.date_lo = CInt(subkey.GetValue("ProfileLoadTimeLow", 0))
                                ProfileName = Path.GetFileName(ImagePath)
                                If Not IsNothing(RawSID) Then
                                    UserID = Misc.SidToName(RemotePC, RawSID)
                                Else
                                    ' If we don't have an imbedded SID, then we
                                    ' use the key name instead
                                    UserID = Misc.SidToName(RemotePC, keyname)
                                End If

                                ' Let's mount the user's hive (if it isn't
                                ' already mounted)
                                we_mounted_it = False
                                If admin AndAlso Array.IndexOf(LoadedHives, keyname) < 0 Then
                                    ' Since we're doing this remotely, the path
                                    ' to the hive is relative to the remote PC
                                    If Misc.MountHive(RemotePC, keyname, ImagePath & "\NTUSER.DAT") = 0 Then
                                        we_mounted_it = True
                                        ' we tag the ones that we mounted with
                                        ' an asterisk
                                        ProfileList.Add(ImagePath, "*" & keyname)
                                    End If
                                End If

                                dr = ds.Profiles.NewProfilesRow
                                dr.ID_Profile = ID
                                dr.Profile = Left(ProfileName, ds.Profiles.ProfileColumn.MaxLength)
                                dr.Profile_Path = Left(ImagePath, ds.Profiles.Profile_PathColumn.MaxLength)
                                If UserID <> "" Then
                                    dr.Profile_Owner = Left(UserID, ds.Profiles.Profile_OwnerColumn.MaxLength)
                                End If

                                If OSVer = 4.0 Or admin = False Then
                                    ' This is a bit crude, but it's better than
                                    ' nothing
                                    dr.Profile_IsAdmin = False
                                    If Not IsNothing(ds.Admins.FindByID_AdminAdmin_UserID(ID, UserID)) Then
                                        dr.Profile_IsAdmin = True
                                    End If
                                Else
                                    dr.Profile_IsAdmin = Misc.IsRemoteAdmin(MachineName, reg_hku, UserID)
                                End If
                                If ld.Long64 > 0 Then
                                    ' Not supported in WinNT or WinVista
                                    dr.Profile_LastLoad = Date.FromFileTime(ld.Long64)
                                End If
                                ds.Profiles.AddProfilesRow(dr)

                                ' Let's do a "test run" to verify that the
                                ' hive got mounted and that current user has
                                ' permission to read it.  We do this here
                                ' rather than in each one of the "dependent"
                                ' methods.
                                Try
                                    testkey = reg_hku.OpenSubKey(keyname & "\Control Panel", False)
                                    If Not IsNothing(testkey) Then
                                        testkey.Close()
                                    End If
                                    If we_mounted_it = False Then
                                        ProfileList.Add(ImagePath, keyname)
                                    End If
                                Catch
                                    ' if this test fails, then this key
                                    ' won't appear in the ProfileList
                                End Try
                            End If
                        End If
                        subkey.Close()
                    Next

                    DefaultUserProfile = key.GetValue("DefaultUserProfile", "Default User").ToString
                    key.Close()

                    ' Update the PC table's NumUser field
                    If ds.PC.Count > 0 Then
                        ds.PC(0).PC_NumUsers = num_users
                    End If

                    ' manually create an entry for the Default User
                    If ds.Profiles.Count > 0 Then
                        Dim parts() As String

                        dr = ds.Profiles.NewProfilesRow
                        dr.ID_Profile = ID
                        dr.Profile = DefaultUserProfile

                        ' use an existing entry as a prototype
                        parts = ds.Profiles(0).Profile_Path.Split("\"c)
                        parts(parts.Length - 1) = DefaultUserProfile
                        ImagePath = Join(parts, "\")
                        dr.Profile_Path = Left(ImagePath, ds.Profiles.Profile_PathColumn.MaxLength)
                        dr.Profile_IsAdmin = False
                        ds.Profiles.AddProfilesRow(dr)

                        we_mounted_it = False
                        If admin AndAlso Array.IndexOf(LoadedHives, DefaultUserProfile) < 0 Then
                            ' Since we're doing this remotely, the path
                            ' to the hive is relative to the remote PC
                            If Misc.MountHive(RemotePC, DefaultUserProfile, ImagePath & "\NTUSER.DAT") = 0 Then
                                we_mounted_it = True
                                ' we tag the ones that we mounted with
                                ' an asterisk
                                ProfileList.Add(ImagePath, "*" & DefaultUserProfile)
                            End If
                        End If
                    End If
                End If
            Catch ex As Exception
                dr = ds.Profiles.NewProfilesRow
                dr.ID_Profile = ID
                dr.Profile = "Error"
                dr.Profile_IsAdmin = False
                dr.Profile_Path = Left("Error: " & ex.Message, ds.Profiles.Profile_PathColumn.MaxLength)
                ds.Profiles.AddProfilesRow(dr)
                Misc.ErrorLog(ex, MachineName, "Profiles", Misc.ErrLogLevel.Errors)
            End Try
        End If
    End Sub

    'BOOL LookupAccountSid(
    '  LPCTSTR lpSystemName,
    '  PSID lpSid,
    '  LPTSTR lpName,
    '  LPDWORD cchName,
    '  LPTSTR lpReferencedDomainName,
    '  LPDWORD cchReferencedDomainName,
    '  PSID_NAME_USE peUse
    ');
    Private Declare Auto Function LookupAccountSid Lib "advapi32.dll" ( _
        ByVal lpSystemName As String, _
        ByVal lpSid As IntPtr, _
        ByVal lpName As String, _
        ByRef cchName As Integer, _
        ByVal lpReferenceDomainName As String, _
        ByRef cchReferencedDomainName As Integer, _
        ByRef peUse As Integer _
    ) As Boolean

    '
    ' Take a "raw" SID (as a byte array) and determine the User ID (used in
    ' GetProfiles) (overloaded)
    '
    Public Shared Function SidToName(ByVal RemotePC As String, ByVal RawSid As Byte()) As String
        Dim Sid As IntPtr
        Dim gch As GCHandle
        Dim ans As String

        ' get a safe pointer to the SID array of bytes
        gch = GCHandle.Alloc(RawSid, GCHandleType.Pinned)
        Sid = Marshal.UnsafeAddrOfPinnedArrayElement(RawSid, 0)
        ans = SidToName(RemotePC, Sid)
        gch.Free()

        Return ans
    End Function

    '
    ' Take a String version of a SID and generate a User ID (used in
    ' GetProfiles) (overloaded)
    '
    Public Shared Function SidToName(ByVal RemotePC As String, ByVal sid_string As String) As String
        Dim sid As IntPtr

        sid = StringToSID(sid_string)
        If IsNothing(sid) Then
            Return sid_string
        End If

        Return SidToName(RemotePC, sid)
    End Function

    '
    ' Take an IntPtr SID and generate the User ID (overloaded)
    '
    Private Shared Function SidToName(ByVal RemotePC As String, ByVal sid As IntPtr) As String
        Dim UserName, name, domain_name As String
        Dim name_len, domain_len, peUse As Integer

        name_len = NAME_SIZE
        domain_len = NAME_SIZE
        name = Space(name_len)
        domain_name = Space(domain_len)

        ' look up the Account associated with that SID
        If LookupAccountSid(RemotePC, sid, name, name_len, domain_name, domain_len, peUse) = False Then
            ' if the lookup fails, return the SID as as string
            Return SIDtoString(sid)
        End If

        ' put the name parts together
        If domain_len > 0 Then
            UserName = Left(domain_name, domain_len) & "\" & Left(name, name_len)
        Else
            If RemotePC = "" Then
                RemotePC = Environment.MachineName
            End If
            UserName = RemotePC & "\" & Left(name, name_len)
        End If

        Return UserName
    End Function

If you want to see the whole thing in action, you can download the complete application that this sample comes from.  (it might be a bit of overkill, but what the heck)  http://www.dpw.hood.army.mil/ftp/SOSOS/
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17111220
This line:

ans = SidToName(RemotePC, Sid)

Gives the error: value of type 'System.IntPtr' cannot be converted to '1-dimensional array of Byte'.

0
 
LVL 96

Expert Comment

by:Bob Learned
ID: 17111560
Here is a WMI answer, using the Win32_NetworkLoginProfile:

' Add a reference to System.Management.dll.

Imports System.Management

Public Class Win32_NetworkLoginProfile

  Public Caption As String
  Public Description As String

  Public Shared Function GetList() As Win32_NetworkLoginProfile()

    Dim query As String = "Select * From Win32_NetworkLoginProfile"

    Dim searcher As New ManagementObjectSearcher(query)

    Dim Profiles As ManagementObjectCollection = searcher.Get()

    Dim listProfiles(Profiles.Count - 1) As Win32_NetworkLoginProfile

    Dim count As Integer = 0

    For Each entryCurrent As ManagementObject In Profiles

      Dim profile As New Win32_NetworkLoginProfile

      profile.Caption = entryCurrent.Properties("Caption").Value
      profile.Description = entryCurrent.Properties("Description").Value

      listProfiles(count) = profile

      count += 1

    Next entryCurrent

    Return listProfiles

  End Function

End Class

Test code:
    Dim builder As New System.Text.StringBuilder

    For Each profile As Win32_NetworkLoginProfile In Win32_NetworkLoginProfile.GetList()
      builder.Append(profile.Caption & vbTab & profile.Description & vbNewLine)
    Next profile

    MessageBox.Show(builder.ToString())

Bob
0
 
LVL 41

Expert Comment

by:graye
ID: 17111615
The SidToName method is overloaded (3 different signatures for the same function)....  one takes a Byte(), one takes an String, and one takes a IntPtr.   You need to include them all.

But hang on... I just did a "cut-n-paste" from a working application to demontrate the concepts.  (I wasn't expecting it to compile... after all, I was just trying to explain how you'd do something similar in your code).

On the other hand, you *can* download the whole application (which obviously will compile).
0
 
LVL 41

Expert Comment

by:graye
ID: 17111643
Bob... The WMI classes only give you the psuedo-accounts and the currently logged in user's profile.  It will not enumerate profiles that are not currently loaded.
0
Highfive Gives IT Their Time Back

Highfive is so simple that setting up every meeting room takes just minutes and every employee will be able to start or join a call from any room with ease. Never be called into a meeting just to get it started again. This is how video conferencing should work!

 
LVL 13

Author Comment

by:usachrisk1983
ID: 17111653
Graye: I was actually just taking what I needed, and excluding all else.  I appreciate your assistance, but I think the following solution is going to work for me.

Bob, your solution worked perfectly, and I can't believe I didn't run immediately to WMI for this, since I tend to work in VBScript.
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17111662
Ooops, did I then accept too soon?

Graye, what type of profiles do you not expect to showwith this?
0
 
LVL 96

Expert Comment

by:Bob Learned
ID: 17111670
@graye,
Good to know Emmet, I thought that was the only accounts that I have profiles for, but it appears that I have more.

Bob
0
 
LVL 41

Expert Comment

by:graye
ID: 17111673
Naaa... WMI is not the answer

Would you like us to write you a fully working sample?   I can do that sometime later this evening (or perhaps tomorrow)
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17111699
Thanks for saving me a post to Community Support, LearnedOne ;)

If adding the remaining two SID functions will fix it, then I will give that a go.  
0
 
LVL 96

Expert Comment

by:Bob Learned
ID: 17111746
Would this help?

different results from Win32_NetworkLoginProfile
http://www.scriptinganswers.com/Community/DiscussionForums/tabid/154/forumid/6/postid/4179/view/topic/Default.aspx

Server Stops Responding When Win32_NetworkLoginProfile Performs Enumeration
http://support.microsoft.com/kb/816485

Bob
0
 
LVL 41

Expert Comment

by:graye
ID: 17112125
Yeah, but.... the SeRestorePrivilege is typically only provided to members of the Administrators / Backup Operators group, so Power Users / Users would not be able to use that privilege and therefore would not be able to enumerate all of the profiles.
0
 
LVL 41

Accepted Solution

by:
graye earned 500 total points
ID: 17112320
Okey dokey... here is a complete working sample (without having do dig thru all of the extra junk in my original example)

You'd use this class like this:

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim buf, ans As String
        Dim lp As New ListProfiles

        ans = ""
        ' Get profiles for the local PC
        For Each buf In lp.GetProfiles("")
            ans &= buf & vbCr
        Next

        MsgBox(ans)
    End Sub

The class file is the following:

Imports Microsoft.Win32
Imports System.Collections.Specialized
Imports System.Runtime.InteropServices

Public Class ListProfiles

    'BOOL LookupAccountSid(
    '  LPCTSTR lpSystemName,
    '  PSID lpSid,
    '  LPTSTR lpName,
    '  LPDWORD cchName,
    '  LPTSTR lpReferencedDomainName,
    '  LPDWORD cchReferencedDomainName,
    '  PSID_NAME_USE peUse
    ');
    Private Declare Auto Function LookupAccountSid Lib "advapi32.dll" ( _
        ByVal lpSystemName As String, _
        ByVal lpSid As IntPtr, _
        ByVal lpName As String, _
        ByRef cchName As Integer, _
        ByVal lpReferenceDomainName As String, _
        ByRef cchReferencedDomainName As Integer, _
        ByRef peUse As Integer _
    ) As Boolean

    'BOOL ConvertStringSidToSid(
    '  LPCTSTR StringSid,
    '  PSID* Sid
    ');
    Private Declare Auto Function ConvertStringSidToSid Lib "advapi32.dll" ( _
        ByVal StringSid As String, _
        ByRef Sid As IntPtr _
    ) As Boolean

    'BOOL ConvertSidToStringSid(
    '  PSID Sid,
    '  LPTSTR* StringSid
    ');
    Private Declare Auto Function ConvertSidToStringSid Lib "advapi32.dll" ( _
        ByVal Sid As IntPtr, _
        ByRef StringSid As IntPtr _
    ) As Boolean

    Private Const NAME_SIZE As Integer = 64

    Public Function GetProfiles(ByVal RemotePC As String) As StringCollection
        Dim ans As New StringCollection
        Dim reg_hklm, key, subkey As RegistryKey
        Dim keyname, UserID As String
        Dim RawSID() As Byte

        ' open the registry (could be remote)
        If RemotePC <> "" Then
            reg_hklm = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, RemotePC)
        Else
            reg_hklm = Registry.LocalMachine
        End If

        ' Get the list of profiles from the registry
        key = reg_hklm.OpenSubKey("SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList", False)
        If Not IsNothing(key) Then
            ' loop thru each subket
            For Each keyname In key.GetSubKeyNames
                subkey = key.OpenSubKey(keyname, False)
                RawSID = CType(subkey.GetValue("SID", Nothing), Byte())

                If Not IsNothing(RawSID) Then
                    UserID = SidToName(RemotePC, RawSID)
                Else
                    ' If we don't have an imbedded SID, then we
                    ' use the key name instead
                    UserID = SidToName(RemotePC, keyname)
                End If

                ' add the UserID to our String Collection
                ans.Add(UserID)
            Next
        End If

        Return ans
    End Function


    '
    ' Take a "raw" SID (as a byte array) and determine the User ID (used in
    ' GetProfiles) (overloaded)
    '
    Private Function SidToName(ByVal RemotePC As String, ByVal RawSid As Byte()) As String
        Dim Sid As IntPtr
        Dim gch As GCHandle
        Dim ans As String

        ' get a safe pointer to the SID array of bytes
        gch = GCHandle.Alloc(RawSid, GCHandleType.Pinned)
        Sid = Marshal.UnsafeAddrOfPinnedArrayElement(RawSid, 0)
        ans = SidToName(RemotePC, Sid)
        gch.Free()

        Return ans
    End Function

    '
    ' Take a String version of a SID and generate a User ID (used in
    ' GetProfiles) (overloaded)
    '
    Private Function SidToName(ByVal RemotePC As String, ByVal sid_string As String) As String
        Dim sid As IntPtr

        sid = StringToSID(sid_string)
        If IsNothing(sid) Then
            Return sid_string
        End If

        Return SidToName(RemotePC, sid)
    End Function

    '
    ' Take an IntPtr SID and generate the User ID (overloaded)
    '
    Private Function SidToName(ByVal RemotePC As String, ByVal sid As IntPtr) As String
        Dim UserName, name, domain_name As String
        Dim name_len, domain_len, peUse As Integer

        name_len = NAME_SIZE
        domain_len = NAME_SIZE
        name = Space(name_len)
        domain_name = Space(domain_len)

        ' look up the Account associated with that SID
        If LookupAccountSid(RemotePC, sid, name, name_len, domain_name, domain_len, peUse) = False Then
            ' if the lookup fails, return the SID as as string
            Return SIDtoString(sid)
        End If

        ' put the name parts together
        If domain_len > 0 Then
            UserName = Left(domain_name, domain_len) & "\" & Left(name, name_len)
        Else
            If RemotePC = "" Then
                RemotePC = Environment.MachineName
            End If
            UserName = RemotePC & "\" & Left(name, name_len)
        End If

        Return UserName
    End Function

    '
    ' Convert a "raw" SID (as a byte array) into a string (overloaded)
    '
    Private Function SIDtoString(ByVal sidbyte As Byte()) As String
        Dim sid As IntPtr
        Dim gch As GCHandle
        Dim ans As String

        ' get a safe pointer
        gch = GCHandle.Alloc(sidbyte, GCHandleType.Pinned)
        sid = Marshal.UnsafeAddrOfPinnedArrayElement(sidbyte, 0)
        ans = SIDtoString(sid)
        gch.Free()

        Return ans
    End Function

    ' Convert a SID into the string version of the SID
    '
    Private Shared Function SIDtoString(ByVal sid As IntPtr) As String
        Dim ans As String
        Dim pStr As IntPtr

        ConvertSidToStringSid(sid, pStr)
        ans = Marshal.PtrToStringAuto(pStr)
        Marshal.FreeHGlobal(pStr)

        Return ans
    End Function

    '
    ' Convert a string representation of a SID into a real SID
    '
    Private Function StringToSID(ByVal buf As String) As IntPtr
        Dim sid As IntPtr

        If ConvertStringSidToSid(buf, sid) = False Then
            Return Nothing
        End If

        Return sid
    End Function
End Class
0
 
LVL 13

Author Comment

by:usachrisk1983
ID: 17123724
Worked great, and increaing points to 500 :)
0

Featured Post

6 Surprising Benefits of Threat Intelligence

All sorts of threat intelligence is available on the web. Intelligence you can learn from, and use to anticipate and prepare for future attacks.

Join & Write a Comment

Article by: jpaulino
XML Literals are a great way to handle XML files and the community doesn’t use it as much as it should.  An XML Literal is like a String (http://msdn.microsoft.com/en-us/library/system.string.aspx) Literal, only instead of starting and ending with w…
Creating an analog clock UserControl seems fairly straight forward.  It is, after all, essentially just a circle with several lines in it!  Two common approaches for rendering an analog clock typically involve either manually calculating points with…
Internet Business Fax to Email Made Easy - With eFax Corporate (http://www.enterprise.efax.com), you'll receive a dedicated online fax number, which is used the same way as a typical analog fax number. You'll receive secure faxes in your email, fr…
When you create an app prototype with Adobe XD, you can insert system screens -- sharing or Control Center, for example -- with just a few clicks. This video shows you how. You can take the full course on Experts Exchange at http://bit.ly/XDcourse.

760 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

23 Experts available now in Live!

Get 1:1 Help Now