Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Get a list of selected files in Explorer (WIndows 7)

At work I can select multiple .xlsx files, and right clicking on one file will give me the option of combining the files to make a .pdf. Now I want to use the same functionality in one of my scripts. That is, selecting multiple files and send the paths of these files as arguments to a Python script.

I've spent soon an hour searching for solutions, but I haven't found any good answers. It seems there are some C# answers, but I don't know how to convert the code to Python. Is it even possible to achieve it at all?

Edit - Sample script:

import sys, os
for file in sys.argv:
    print(file)
os.system("PAUSE")
like image 862
Olav Avatar asked Feb 15 '23 01:02

Olav


2 Answers

I know this is a "bit" late to post an answer here, but I had tried Olav's solution some months ago and it didn't work completely: the working directory was the script's working directory, so I had to delete the if condition for it to work, but it selected all the files in all Windows Explorer windows (which I wanted too, so it worked partially for me). But after coming back to this, I thought on this idea. I don't know if this answer worked for anyone else, but for me it didn't completely so I thought I could improve it and post my solution here.

This code is a mix of this answer by James Kent: https://stackoverflow.com/a/43892579/8228163 (corrected by me to work at least under Windows 7) and Olav's answer and the result worked for me - the script detects the files only in the current Windows Explorer window. I think all of this works from Vista (not sure, running 7 here) to 10, but I'm not completely sure. The other answer was made to work with XP. When I started this script on Windows 10, I think it worked, but I don't have 10 anymore so I don't know for sure (I'm using 7 again, so for 7 this works).

import win32gui, time
from win32con import PAGE_READWRITE, MEM_COMMIT, MEM_RESERVE, MEM_RELEASE, PROCESS_ALL_ACCESS, WM_GETTEXTLENGTH, WM_GETTEXT
from commctrl import LVS_OWNERDATA, LVM_GETITEMCOUNT, LVM_GETNEXTITEM, LVNI_SELECTED
import os
import struct
import ctypes
import win32api
import datetime
import win32com.client as win32
import win32ui
import psutil
import subprocess
import time
import urllib.parse

clsid = '{9BA05972-F6A8-11CF-A442-00A0C90A8F39}' #Valid for IE as well!

def getEditText(hwnd):
    # api returns 16 bit characters so buffer needs 1 more char for null and twice the num of chars
    buf_size = (win32gui.SendMessage(hwnd, WM_GETTEXTLENGTH, 0, 0) +1 ) * 2
    target_buff = ctypes.create_string_buffer(buf_size)
    win32gui.SendMessage(hwnd, WM_GETTEXT, buf_size, ctypes.addressof(target_buff))
    return target_buff.raw.decode('utf16')[:-1]# remove the null char on the end

def _normaliseText(controlText):
    '''Remove '&' characters, and lower case.
    Useful for matching control text.'''
    return controlText.lower().replace('&', '')

def _windowEnumerationHandler(hwnd, resultList):
    '''Pass to win32gui.EnumWindows() to generate list of window handle,
    window text, window class tuples.'''
    resultList.append((hwnd, win32gui.GetWindowText(hwnd), win32gui.GetClassName(hwnd)))

def searchChildWindows(currentHwnd,
               wantedText=None,
               wantedClass=None,
               selectionFunction=None):
    results = []
    childWindows = []
    try:
        win32gui.EnumChildWindows(currentHwnd,
                      _windowEnumerationHandler,
                      childWindows)
    except win32gui.error:
        # This seems to mean that the control *cannot* have child windows,
        # i.e. not a container.
        return
    for childHwnd, windowText, windowClass in childWindows:
        descendentMatchingHwnds = searchChildWindows(childHwnd)
        if descendentMatchingHwnds:
            results += descendentMatchingHwnds

        if wantedText and \
            not _normaliseText(wantedText) in _normaliseText(windowText):
                continue
        if wantedClass and \
            not windowClass == wantedClass:
                continue
        if selectionFunction and \
            not selectionFunction(childHwnd):
                continue
        results.append(childHwnd)
    return results


def explorer_fileselection():
    global clsid
    address_1=""
    files = []
    shellwindows = win32.Dispatch(clsid)
    w=win32gui
    window = w.GetForegroundWindow()
    #print("window: %s" % window)
    if (window != 0):
        if (w.GetClassName(window) == 'CabinetWClass'): # the main explorer window
            #print("class: %s" % w.GetClassName(window))
            #print("text: %s " %w.GetWindowText(window))
            children = list(set(searchChildWindows(window)))
            addr_edit = None
            file_view = None
            for child in children:
                if (w.GetClassName(child) == 'WorkerW'): # the address bar
                    addr_children = list(set(searchChildWindows(child)))
                    for addr_child in addr_children:
                        if (w.GetClassName(addr_child) == 'ReBarWindow32'):
                            addr_edit = addr_child
                            addr_children = list(set(searchChildWindows(child)))
                            for addr_child in addr_children:
                                if (w.GetClassName(addr_child) == 'Address Band Root'):
                                    addr_edit = addr_child
                                    addr_children = list(set(searchChildWindows(child)))
                                    for addr_child in addr_children:
                                        if (w.GetClassName(addr_child) == 'msctls_progress32'):
                                            addr_edit = addr_child
                                            addr_children = list(set(searchChildWindows(child)))
                                            for addr_child in addr_children:
                                                if (w.GetClassName(addr_child) == 'Breadcrumb Parent'):
                                                    addr_edit = addr_child
                                                    addr_children = list(set(searchChildWindows(child)))
                                                    for addr_child in addr_children:
                                                        if (w.GetClassName(addr_child) == 'ToolbarWindow32'):
                                                            text=getEditText(addr_child)
                                                            if "\\" in text:
                                                                address_1=getEditText(addr_child)[text.index(" ")+1:]
                                                                print("Address --> "+address_1)

    for window in range(shellwindows.Count):
        window_URL = urllib.parse.unquote(shellwindows[window].LocationURL,encoding='ISO 8859-1')
        window_dir = window_URL.split("///")[1].replace("/", "\\")
        print("Directory --> "+window_dir)
        if window_dir==address_1:
            selected_files = shellwindows[window].Document.SelectedItems()
            for file in range(selected_files.Count):
                files.append(selected_files.Item(file).Path)
            print("Files --> "+str(files))

while True:
    explorer_fileselection()
    time.sleep(1)

This looks for the active Windows Explorer window, gets that window's address and then the address is used on Olav's answer for it to check if that address is equal to one of the addresses opened in Windows Explorer, getting the files from the active window. Btw, as this is script is a modified copy of both answers, it has their limitations:

  • Like it's on Olav's answer "Edit: Doesn't work yet, at least when using context menu", then this won't probably work either, as it's the same code - it's just the working directory which is different (though, I don't know what he meant with that, but from what I've tested, it worked).
  • And like it's on James Kent's answer, this doesn't work for desktop, only for opened windows using Windows Explorer. The encoding='ISO 8859-1' is because I'm portuguese, but it can be changed, JUST make sure both directories are equal without %?s or that won't work!

Note: for Windows XP, see the the original answer (https://stackoverflow.com/a/43892579/8228163) and to get the files from all Windows Explorer windows, just remove the if condition from Olav's answer.

Thanks Olav and James Kent for the answers, because I would have taken MUCH more time trying to find out how to do this (I'm a Python/any language very begginner).

Hope this helps! Cheers!

like image 200
Edu590 Avatar answered Mar 06 '23 20:03

Edu590


Edit: Doesn't work yet, at least when using context menu

I found a partial solution here. However it doesn't work if Internet Explorer is open (how do I detect this?). Also, if multiple instances of Windows Explorer is open, the selected files in all windows are counted. I added a check for this.

import win32com.client as win32
import os
import win32ui


def explorer_fileselection():
    working_dir = os.getcwd()

    clsid = '{9BA05972-F6A8-11CF-A442-00A0C90A8F39}' #Valid for IE as well!
    shellwindows = win32.Dispatch(clsid)

    files = []
    try:
        for window in range(shellwindows.Count):
            window_URL = shellwindows[window].LocationURL
            window_dir = window_URL.split("///")[1].replace("/", "\\")
            if window_dir == working_dir:
                selected_files = shellwindows[window].Document.SelectedItems()
                for file in range(selected_files.Count):
                    files.append(selected_files.Item(file).Path)
    except:   #Ugh, need a better way to handle this one
        win32ui.MessageBox("Close IE!", "Error")
    del shellwindows

    return files


print(*explorer_fileselection(), sep="\n")

--- prints the selected files:

C:\Users\oe\Python\ssa\util\test3.docx
C:\Users\oe\Python\ssa\util\__init__.py
C:\Users\oe\Python\ssa\util\explorer_filer.py
C:\Users\oe\Python\ssa\util\test1.xlsx
C:\Users\oe\Python\ssa\util\test2.xls

I think I will add a *valid_ext parameter to the function, so I can do calls like explorer_fileselection("xlsx", "xls") to get Excel-files only.

like image 22
Olav Avatar answered Mar 06 '23 21:03

Olav