-3
$\begingroup$

The link below by poor is very relevant.

This is also related: How can i open a file select dialog via python to add an image sequence into VSE?

The best source I have found in 4 days of search is:

https://blenderapi.wordpress.com/2011/09/26/file-selection-with-python/#comment-4

I had copied the code from the existing Blender UI with minor edit

strip = context.scene.render

layout.prop(strip, "filepath", text="")

which works, but isn't wise because it is where the render output path is stored, so re-using this changes the render output path.

$\endgroup$
  • $\begingroup$ Does this help: blender.stackexchange.com/questions/26898/… ? $\endgroup$ – p2or Dec 7 '15 at 13:12
  • $\begingroup$ Will post. Not at workstation right now. This is my first Blender & first python coding. Up till 2am last night amazed that I finally got the file browser to work. Also amazed that the main script works. Amazed at how I swear when coding. $\endgroup$ – The Beachdancer Dec 7 '15 at 13:52
  • $\begingroup$ Poor, thanks, but that seems to be putting the tool in the VSE window. I understand that sounds relevant, but isn't really. The script "Audio bake Concert Style' creates hundreds of objects in the 3d view that have a single axis baked to individual notes based on concert pitch A4=440Hz music tr. $\endgroup$ – The Beachdancer Dec 7 '15 at 13:56
  • 1
    $\begingroup$ I have it in TOOLS with its own tab called "Concert Style". You understand that this is all done? I raised the question because I found it very hard to find information, and felt that putting here as a question would help others to find the solution faster. I completed this part of the code last night at 2am. There are odd things about it and some improvements for later. My immediate tasks are to combine the UI part, which I wrote separately, with the main code and tackle the issues this raises. Then improve the input with integer inputs for some of the variables. Don't know yet how to do. $\endgroup$ – The Beachdancer Dec 7 '15 at 14:12
6
$\begingroup$

There are several approaches to this, all quite similar except for where the data is stored. A preferred approach is to store your add-on's properties as a group rather than all as Scene properties.

Register a Property Group

Worth reading the docs and this answer too: https://blender.stackexchange.com/a/6977/47

# adding a PropertyGroup helps keep the Scene namespace clean.
# instead of adding 10 properties directly to Scene, you only
# add 1 group, which itself has a dot(.) lookup to get individual
# properties

class MyAddonProperties(bpy.types.PropertyGroup):
    some_identifier = bpy.props.StringProperty()
    some_integer_prop = bpy.props.IntProperty()
    # ...etc ..

Here's a full but short add-on that uses a Property Group. There's a few things to understand. A Property Group is a class and needs be registered and unregistered. You can do it explicitly using bpy.utils.register_class(ClassName) or use a convenience method that registers all bpy classes found in the file in their order of appearance within the file.

The order in which things are done inside the register and unregister fnctions matters and can make a difference

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.my_addon = bpy.props.PointerProperty(type=MyAddonProperties)


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.my_addon

ui

def draw(self, context):
    layout = self.layout
    scn = context.scene

    # ui
    col = layout.column()
    row = col.row(align=True)
    row.prop(scn.my_addon, 'some_identifier', text='directory:')
    row.operator("something.identifier_selector", icon="FILE_FOLDER", text="")

The FileSelector Operator

class WMFileSelector(bpy.types.Operator, ExportHelper):
    bl_idname = "something.identifier_selector"
    bl_label = "some folder"

    filename_ext = ""

    def execute(self, context):
        fdir = self.properties.filepath
        context.scene.my_addon.some_identifier = fdir
        return{'FINISHED'}
$\endgroup$
1
$\begingroup$
    import bpy

    # Opens a web browser and prints the filepath to console. 
    #(To see console.. Window >>> Toggle system console


    class OpenBrowser(bpy.types.Operator):
        bl_idname = "open.browser"
        bl_label = "Minimum code to open browser & get filepath"

        filepath = bpy.props.StringProperty(subtype="FILE_PATH") 
        #somewhere to remember the address of the file


        def execute(self, context):
            display = "filepath= "+self.filepath  
            print(display) #Prints to console  
            #Window>>>Toggle systen console

            return {'FINISHED'}

        def invoke(self, context, event): # See comments at end  [1]

            context.window_manager.fileselect_add(self) 
            #Open browser, take reference to 'self' 
            #read the path to selected file, 
            #put path in declared string type data structure self.filepath

            return {'RUNNING_MODAL'}  
            # Tells Blender to hang on for the slow user input


    bpy.utils.register_class(OpenBrowser) 
    #Tell Blender this exists and should be used


    # [1] In this invoke(self, context, event) is being triggered by the below command
    #but in your script you create a button or menu item. When it is clicked
    # Blender runs   invoke()  automatically.

    #execute(self,context) prints self.filepath as proof it works.. I hope.


    bpy.ops.open.browser('INVOKE_DEFAULT')

That's it.

Only read on if you want more info on building a UI panel.

Building a Blender User interface using python scripting

The individual commands to create buttons, input sliders and file browsers only work if put into the structure that Blender requires. If used on their own the best result is an error message from the syntax checker and the worst is that Blender crashes.

Creating a new panel in which the user can see data, change data, or select a file requires code that mimics the independent panel.

The panel is described as an object and the code that creates it and responds to it is put in one place also described as an object.

This collection of panel related code is defined as a class.

To code a class, that reserved word 'class' is written all lower case followed by a name that means something to you, but which won't matter much to Blender. The convention is to write it in CamelCaps to help recognise class names in code.

After the lower case class and the CamelCaps user name comes the reserved standard method of telling Blender that this is where the user interface panel is defined and handled. The code for telling Blender 'hey this is a UI panel' is (bpy.types.Panel)

    class UIBuild (bpy.types.Panel):

[Note: UPPERCASE and lowercase matter in the Python code language. The above works, but the following both produce a hard to spot syntax error failure....

    Class UIBuild (bpy.types.Panel):    class UIBuild (bpy.types.panel):  

Also remember the colon. ]

Blender needs your Panel class to have some standard information including which window and which existing panel it is to be attached to.

    bl_label = "Music baked Concert Style"  #Printed out on panel
    bl_idname = "ui.build"                                  #Internal Blender name lower case one dot
    bl_space_type = 'VIEW_3D'                         #The window to use
    bl_region_type = 'TOOLS'                             # Left hand T panel
    bl_category = 'ConcertStyle'                        #User created tab -default is: Misc

That would create a panel with: Music baked Concert Style :printed across the top, but the code will trigger an error because Blender expects to find more than this.

The buttons, labels, variable sliders and filebrowser opening are drawn onto the panel in a reserved word function called: draw(self, context): :that has to be coded within the class Panel.

   def draw(self,context): 

is the standard function in which you code what the user is going to see and be able to click on in a panel in one of the windows.

To draw some text onto the panel

    self.layout.row.label(text="This will be written on the panel")

Text plus an icon:

    self.layout.row.label(text="Select audio track to load",icon="SPEAKER")

A single clickable button, with text and updating data value & an icon:

    self.layout.row.operator( text=Message ,icon="NLA")

(To get the variable data to be printed on the button the above needs to be preceeded with a definition of that Message such as

    Message="Click here to load this instrumental track. Tracks="+str(Tracks)

Where Tracks is a number and is converted into a string by str(Tracks) which is then added on to the actual string. It can then be printed on the button and can be updated automatically if the variable changes. )

With multiple buttons each of which is to call different code there is the need to make the buttons distinguisable. That requires a separate function that defines buttons as having some added feature like a reference number attached to them. Then the code that reacts to button clicks can check which button was clicked. (See below)

Clickable items drawn on the panel are made by that word operator .

When the mouse hovers over a clickable button the draw(self,context) function is called and the buttons are redrawn. That is how and when the displayed data updates. (No hover, no update)

Whenever a button is clicked Blender looks for a function within class Panel to react to that click. First it looks for a function called

    invoke(self,context,event)

invoke(self,context,event): is a standard function which automatically runs whenever a user defined button is clicked.

If there is only a single button, whatever code is written within

    invoke(self,context,event):   

runs to react to that button, but if there is more than 1 button the buttons need to be identifiable so that different actions can be triggerred by each. (See below)

If the button is to open a filebrowser the command is:

    context.window_manager.fileselect_add(self)

(If used on its own outside of invoke(self,context,event) is likely to crash Blender.)

To keep the browser window open and Blender not crashing the invoke function has to also send back a message about how it should be running: return{'RUNNING_MODAL'}.

    invoke(self,context,event):

        context.window_manager.fileselect_add(self) 


     return{'RUNNING_MODAL'}

(If there are two buttons, no matter what you want the second one to do it is going to open the file browser.) (For solution see below)

If Blender doesn't find an invoke() function it will settle for an execute() function, but usually execute is where it looks when values are changed by the user.

    execute(self,context)

Called when user changes a value in the panel rather than clicking a button.

Within draw(self,context): to draw an input for numbers with sliders the command is:

well actually there isn't a command for that. It gets complicated. First the variable has to be named in a formal declaration of its type and some other detals. After that the command to use is:

    layout.prop(context.scene, 'TotalNotes')        #TotalNotes was part of the declaration


    layout.prop(context.scene, 'ObjectXScale')     # ObjectXScale is the internal ID for this variable.

Blender uses that single command, together with the declaration of the variable, to create a titled numerical input with up and down arrows, click-to-enter dialogue of the relevant type such as integer (whole numbers), float (numbers with fractions), vector (3 number system)
(See below for how to declare the variable)

What can be done in each function:

Draw() The expand/collapse triangle is standard Text title and an icon which is displayed at top Text and icons in rest of panel Clickable buttons with text and icon Clickable file browser Integer, float or vector input boxes with sliders Layout of these items in rows, columns and sizes.

Invoke(self,context,event) Check which button has been clicked. Decide what function to call in response to that button Return something to a selected function to carry out whatever that function does. Open file browser


To react differently to different buttons the buttons have to be different. A way of defining buttons to be different is to define them as having an identifying reference number attached to them. With an identifying number it is possible to react differently to each button.

    class OBJECT_OT_Button(bpy.types.Operator):
        bl_idname = "but.ton"
        bl_label = "Click bait"
        number = bpy.props.IntProperty() #Buttons of type "but.ton" can store a number and be identified 



    Within draw(self,context):   the command to draw a but.ton type of button is 

    self.layout.row.operator("but.ton", text="I am a but.ton not a number!" ).number=1  #Choose an integer 

[To work Blender has to be told about this definition. Use the command bpy.utils.register_class(OBJECT_OT_Button) ]

To display a variable value on the UI panel and get changes from the user, first the variable has to be declared. Declaring a variable so it can be used to create an input dialogue on the panel:

    bpy.types.Scene.ObjectXScale = FloatProperty( #declares another addition to the data structure
    name = "NotesXScale",   #name is displayed on panel
    description = "Size of the note indicator.",     #tooltip to be displayed
    default = .5,                                                              #the amount displayed in the panel
    min = .01,                                                                  #slider won't go below this
    max = 5)                                                                    #slider won't go above this        



bpy.types.Scene.ObjectYScale = IntProperty( #declares another addition to the data structure                name = "Amplification-Y-IMPORTANT ",     #name is displayed on panel
    description = " It amplifies the effect of the audio",                     #tooltip
    default = 200,                                                         #the amount displayed in the panel
    min = 10,                                                                 #slider won't go below this
    max = 1000)                                                           #slider won't go above this   

There are other types, but the layout is similar.

Small print warnings:

Copying parts of Blender's IU can lead to errors because they are set up to use the existing data structure and if used in that state they will mess with things. Example: if you copy the render panel's open browserr button anything it does is going to also change the render output destination.

Also, that browser button does not call INVOKE

MENU If running a script rather than an addon the menu will be added to every time the script it run.
If run as an addon the menu is only added to once.

Turning a script into an addon IS NOT as easy as many claim. Just putting the header on probably means failure.

$\endgroup$
  • 1
    $\begingroup$ I doubt many will read this unless you format it to be more appealing. Also quite a few remarks here refer to general Python syntax rules more than blender API (Class vs class, capitalization, formatting a string dynamically...) and of course you have to follow Blender's API conventions, but the best way to learn those is to read the content carefully of scripts/addons, and the Addon tutorial (all the way to the end) . --- I also had a heavy time learning how to write add-ons -- it's worth reading add-on sources by ideasman42 (campbell barton). $\endgroup$ – zeffii Dec 10 '15 at 14:58
  • 1
    $\begingroup$ This answer is way out of proportion to the body of the Question that spawned it. I don't mean to discourage you, but the format of Blender.StackExchange works better at solving narrow issues. This seems like it should be perhaps a blog post or a tutorial / walkthrough of your own process of tackling a foreign API. $\endgroup$ – zeffii Dec 11 '15 at 20:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.